Code Formatting and Linting Tools
Covers the standard Go formatting and linting tools such as gofmt, goimports, go vet, staticcheck, and golangci-lint, why they exist, and how to use them.
Go has a strong opinion about how code should look. The language ships with tools that automatically enforce a single canonical style, and the ecosystem provides a rich set of linters that catch bugs and suspicious patterns before they reach production. This page covers the tools you need to keep Go code clean, consistent, and correct.
Why Formatting and Linting Matter
In most programming languages, teams spend time debating indentation, brace placement, and import ordering. Go eliminates those debates entirely: there is one correct format, defined by the gofmt tool, and the community treats any deviation as simply wrong. This saves time and makes all Go code look familiar.
Linting takes this further. A linter is a program that examines source code for potential problems without actually executing it. In Go, linters can detect unused variables, code that will never run, incorrect use of formatting verbs, and much more. While the compiler already rejects many mistakes, linters catch a class of errors the compiler silently accepts—subtle bugs that are easy to miss in code review.
gofmt and go fmt
gofmt reads Go source code and outputs the same program with standard formatting. It does not change the logic; it only adjusts spacing, indentation, line breaks, and alignment according to the official Go style.
The command go fmt is a wrapper around gofmt that applies it to all the Go files in the packages you specify. Running go fmt ./... at the root of a module will reformat every .go file in the module.
Mechanically, gofmt parses Go source into an abstract syntax tree (AST) and then pretty‑prints that AST with the canonical rules. Because it works on the AST level, it can handle even extremely messy code and produce clean, readable output.
Using gofmt Directly
# Show the diff between the file and what gofmt would produce
gofmt -d main.go
# Write the formatted version back to the file
gofmt -w main.go
# List files that are not formatted according to gofmt
gofmt -l .
When gofmt -l . prints nothing, every file already matches the standard format.
All Formatted:
If gofmt -l produces no output, your files are already correctly formatted. This is the goal before every commit.
go fmt in Practice
go fmt accepts the same package patterns as other go commands. It calls gofmt -l -w on the matching files, so it both formats and reports which files it touched.
go fmt ./...
This is the command you are most likely to run by hand or wire into a pre‑commit hook.
How Beginners Should Think About gofmt
Think of gofmt as a spell‑checker for code appearance. You write the logic; the tool makes sure the presentation is correct. Once you get used to it, you stop worrying about where to put spaces and braces, and your editor can run it automatically on every save.
A Common Mistake with gofmt
Running gofmt on Broken Code:
gofmt needs valid Go syntax. If your file has a syntax error, gofmt will fail with a parse error and will not format the file. Fix the syntax error first.
A real‑world example: this snippet has inconsistent indentation and a missing import for fmt.
package main
func main() {
fmt.Println("hello")
x:= 42
print(x)
}
Running gofmt -d main.go shows what would change:
diff -u main.go.orig main.go
--- main.go.orig
+++ main.go
@@ -1,6 +1,11 @@
package main
+import "fmt"
+
func main() {
- fmt.Println("hello")
- x:= 42
- print(x)
+ fmt.Println("hello")
+ x := 42
+ print(x)
}
gofmt added the missing import and fixed the spacing and indentation. Notice that it did not remove the unused variable x—that is a job for a linter, not a formatter.
goimports
goimports is a superset of gofmt that also manages import statements. It adds missing imports, removes unused ones, and organizes them into standard library and external groups.
It was created because gofmt alone does not touch imports at all. A file with a missing "fmt" import and a stray "net/http" that is never used will still be syntactically valid, so gofmt leaves it alone. goimports fixes that.
Install it:
go install golang.org/x/tools/cmd/goimports@latest
It accepts the same flags as gofmt: -w writes back, -d shows a diff, -l lists files that need changes.
// main.go before goimports
package main
import "net/http"
func main() {
fmt.Println("hello")
}
goimports -d main.go outputs a diff that removes "net/http" and adds "fmt". After formatting, the import block is clean and only contains what the code actually uses.
Module Mode is Required:
For goimports to resolve imports, Go modules must be enabled (the directory contains a go.mod). In a GOPATH‑only setup, the tool may not find packages correctly.
goimports is the tool most developers configure their editor to run on save, because it ensures the import block never drifts out of sync with the code.
go vet
go vet is an official linter that ships with Go. It checks for common mistakes that the compiler does not flag, such as:
- Using a
fmt.Printfverb that does not match the argument type - Struct field tags with wrong syntax
- Unreachable code
- Passing
testing.T(or other testing structures) incorrectly - Misuse of
sync/atomicfunctions
Run it with:
go vet ./...
If the command prints nothing, no issues were found.
Consider this example:
package main
import "fmt"
func main() {
name := "world"
fmt.Printf("hello %d\n", name) // should be %s
}
The compiler accepts this, but go vet reports:
./main.go:8:2: fmt.Printf format %d has arg name of wrong type string
The check is conservative: it only flags constructs that are almost certainly wrong, based on the vet checks bundled with the standard distribution. It will not catch unused variables, unreachable code buried deeper in logic, or style issues.
Not a Complete Solution:
go vet is designed to run quickly and with zero configuration, but it catches only a subset of issues. For comprehensive static analysis, combine it with staticcheck or golangci-lint.
staticcheck
staticcheck is a powerful static analysis tool that catches a much wider range of problems than go vet. It detects:
- Unused variables, functions, and fields
- Code that always returns the same value (simplifiable expressions)
- Incorrect uses of
time.Timecomparisons - Deprecated standard library functions
- Deferred calls to
Closethat discard errors - Many more patterns cataloged at staticcheck.io
Install:
go install honnef.co/go/tools/cmd/staticcheck@latest
Run it against a package:
staticcheck ./...
Take this code:
package main
import "time"
func main() {
var t time.Time
if t == time.Now() { // always false
println("now")
}
}
staticcheck outputs:
main.go:8:5: comparison of time.Time with time.Now is always false (SA4010)
The diagnostic code (SA4010) points to a specific check, which you can look up to understand why it fires.
Standalone or Aggregated:
staticcheck can be run directly, but it is also one of the default linters inside golangci-lint. Running it through golangci-lint gives you caching and parallel execution.
golangci-lint
golangci-lint is a meta‑linter that runs dozens of individual linters at once, including staticcheck, go vet, errcheck, and many more. It was built for speed: it runs linters concurrently and caches results so that subsequent runs are nearly instant.
Install:
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
A typical invocation:
golangci-lint run ./...
That command will check all Go files in the module with a sensible default set of linters. If you need to tune which linters are enabled or adjust their severity, create a .golangci.yml configuration file. A minimal example:
linters:
enable:
- govet
- staticcheck
- errcheck
- gosec
- gofmt
- goimports
golangci-lint will use this file automatically when it is present in the project root.
Ignoring Linter Warnings:
It is tempting to disable linters or add //nolint comments when a check is annoying. Each suppression should be reviewed like any other code change—the linter is often right, and disabling it hides a real problem.
The tool is designed for CI pipelines. A typical GitHub Actions step runs golangci-lint run and fails the build on any warning. Because of its speed and ease of configuration, golangci-lint has become the standard linting tool in most professional Go projects.
Editor Integration
While this chapter focuses on the command‑line tools, they are most effective when integrated into your editor. The Go language server gopls already runs many of these checks automatically. Most editor plugins also provide a “format on save” hook that invokes goimports.
For example, in VS Code with the Go extension, enabling the following settings makes formatting and linting transparent:
"editor.formatOnSave": true,
"go.formatTool": "goimports",
"go.lintTool": "golangci-lint"
The key takeaway is that these tools are not meant to be run manually every time. They belong in your save hook, your pre‑commit hook, and your CI pipeline.
Summary
The Go ecosystem ships with a canonical formatter (gofmt) and a lint foundation (go vet), but the community has built a broader suite of tools that make code cleaner and safer. goimports takes care of imports; staticcheck catches deep logic mistakes; and golangci-lint ties everything together with a single, fast command.
The workflow that emerges is: write code, let your editor format it on save with goimports, and let golangci-lint catch problems before you push. If you adopt these habits early, you will never waste time on style discussions or debugging errors a linter could have caught.