gofmt and go vet
Go ships with two small command-line tools that most other languages leave to third-party plugins: gofmt, which rewrites your source code into one canonical layout, and go vet, which reads your already-compiling code and flags patterns that are almost certainly bugs. Together they are why Go code across the entire ecosystem — the standard library, random GitHub repositories, your coworker’s pull request — all looks the same and avoids a long list of well-known footguns. This lesson covers how each tool actually works, how to run them, and the specific mistakes they exist to catch.
Overview: What gofmt and go vet Do
gofmt is a formatter, not a linter. It parses a Go source file into an abstract syntax tree (AST) — the same tree the compiler builds — and then reprints that tree using one fixed set of rules: tabs for indentation, a single space around binary operators, opening braces on the same line as the statement that owns them, aligned struct fields and consecutive assignments, and sorted, grouped import blocks. Because it works from the AST rather than the raw text, gofmt does not care how you originally spaced or indented anything; two files that parse to the same tree always come out identical after formatting. Crucially, gofmt has essentially no configuration options — there is no equivalent of a .prettierrc or .clang-format file. That is deliberate: the Go team decided that one style, chosen once, is more valuable than a style teams can argue over. Every Go programmer’s editor can reformat any Go file the same way, which is why Go diffs rarely contain noise from formatting-only changes.
go vet is a different kind of tool: a static analyzer. Where the compiler only checks that your code is syntactically and type-correct, go vet loads and type-checks your package (so it needs code that already compiles) and then runs a suite of independent analyzers over the AST, each looking for one specific, well-known mistake that is legal Go but almost never what you meant. The default set includes checks such as printf (format-verb and argument mismatches in fmt.Printf-style calls), structtag (malformed struct tags such as a tag written without the colon between key and value), copylocks (copying a struct that embeds a sync.Mutex or similar), unreachable (code after a return, panic, or infinite loop that can never execute), and lostcancel (a context.WithCancel whose cancel function is never called), among others. None of these are compile errors — the program builds and runs regardless — but they are exactly the kind of bug that slips past code review because the code looks fine and the compiler says nothing. go vet is also run automatically, with a curated subset of the harshest checks, every time you run go test, so many Go developers see its warnings without ever typing go vet directly.
Syntax
Both tools are invoked from the command line inside a module (a directory with a go.mod file).
| Command | What it does |
|---|---|
gofmt file.go |
Prints the reformatted file to stdout; the file on disk is untouched. |
gofmt -l . |
Lists every file under the current directory whose formatting differs from gofmt’s output. |
gofmt -d file.go |
Prints a unified diff between the current file and its formatted version. |
gofmt -w file.go |
Rewrites the file in place with the formatted version. |
go fmt ./... |
A thin wrapper around gofmt that operates on whole packages by import path; this is the form most people type day to day. |
| Command | What it does |
|---|---|
go vet ./... |
Runs the default analyzers over every package in the current module and prints any findings to stderr. |
go vet ./pkg/... |
Restricts vet to a specific package path. |
go vet -printf ./... |
Runs only the named analyzer instead of the full default set. |
go test ./... |
Automatically runs a subset of vet’s checks before compiling tests; a vet failure here fails the test run. |
Examples
Example 1: What gofmt normalizes
Here is a file exactly as someone might type it quickly, with inconsistent spacing and no blank lines separating declarations:
package main
import "fmt"
func main() {
name :="Gopher"
if name!=""{
fmt.Println("Hello,",name)
}
}
Output:
Hello, Gopher
That messy version still compiles and runs correctly — whitespace is irrelevant to the compiler. Running gofmt -w main.go rewrites it to the canonical form:
package main
import "fmt"
func main() {
name := "Gopher"
if name != "" {
fmt.Println("Hello,", name)
}
}
Output:
Hello, Gopher
gofmt inserted blank lines around the import block, added spaces around := and !=, added a space after the comma in the argument list, and switched indentation to tabs. None of this changes what the program does — the AST for both versions is identical — it only changes how it looks, which is exactly gofmt’s job.
Example 2: go vet catching a Printf mismatch
This program compiles and runs without any error, but it has a real bug: the format verb does not match the argument type.
package main
import "fmt"
func main() {
name := "Gopher"
fmt.Printf("Hello, %d\n", name)
}
Output:
Hello, %!d(string=Gopher)
fmt.Printf takes its trailing arguments as any, so the compiler cannot check that %d (an integer verb) is being given a string. At run time, the fmt package notices the mismatch itself and prints the %!d(string=Gopher) placeholder instead of crashing. Running go vet on this file catches the mistake before it ever runs:
$ go vet ./...
./main.go:7:2: Printf format %d has arg name of wrong type string
This is the single most common reason to run go vet: type mismatches inside format strings are invisible to go build because format strings are just ordinary string literals as far as the type checker is concerned.
Example 3: go vet catching an accidental lock copy
This example has a subtler bug involving a value receiver on a type that contains a sync.Mutex:
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
count int
}
func (c Counter) Increment() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}
func main() {
c := Counter{}
c.Increment()
fmt.Println(c.count)
}
Output:
0
Because Increment has a value receiver, calling c.Increment() copies the whole Counter — including its sync.Mutex — into the method. The increment happens to the copy, so the original c in main is never touched and still prints 0. Copying a sync.Mutex is also dangerous in its own right: the copy starts out as a fresh, unlocked mutex, so two goroutines could each get their own copy and both believe they hold the lock. go vet flags this directly:
$ go vet ./...
./main.go:13:14: Increment passes lock by value: Counter contains sync.Mutex
The fix is a pointer receiver, which is also required once you actually want the method to mutate the struct:
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
count int
}
func (c *Counter) Increment() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}
func main() {
c := Counter{}
c.Increment()
fmt.Println(c.count)
}
Output:
1
With a pointer receiver, c.Increment() automatically becomes (&c).Increment(), so the method locks and mutates the real Counter in main, and go vet’s copylocks warning disappears.
How gofmt and go vet Work, Step by Step
gofmt:
- Parse the source file into an AST using the same parser package (
go/parser) the compiler uses. - Walk the AST and normalize its structure — for example, canonicalizing how import groups are organized.
- Print the AST back out using the
go/printerpackage, which owns every layout decision: tabs vs. spaces, brace placement, alignment of struct fields and const blocks using tab-stop columns, and blank-line rules between declarations. - Compare the printed output to the original bytes; with
-lit reports a mismatch, with-wit overwrites the file, and with no flags it just prints the result.
go vet:
- Load the target package(s) with the same machinery
go builduses, including full type-checking — this is why go vet needs code that already compiles. - Build a typed representation of the package that each analyzer can inspect.
- Run every enabled analyzer independently over that representation. Each analyzer is self-contained and looks for one narrow pattern, such as a
%dverb paired with a non-integer argument, or a struct literal containing async.Mutexbeing passed by value. - Collect every analyzer’s diagnostics and print them as
file:line:column: message, then exit with a non-zero status if anything was found.
Neither tool changes program behavior on its own — gofmt only touches whitespace and layout, and go vet only reports, it never rewrites code. That separation is intentional: formatting is mechanical and safe to apply automatically, while a vet finding usually needs a human to decide the actual fix.
Common Mistakes
Mistake 1: Hand-aligning code instead of running gofmt
Developers coming from other languages sometimes manually space out struct fields to make them line up, then fight gofmt when it changes the spacing again:
type Point struct {
X int
Y int
Label string
}
gofmt does not preserve hand-tuned spacing — it recomputes alignment itself, using tabs, based on the longest field name in the block:
type Point struct {
X int
Y int
Label string
}
The fix is behavioral, not code: stop hand-formatting and let your editor run gofmt -w (or go fmt ./...) on save. Anything typed by hand gets normalized anyway, so time spent aligning columns manually is wasted.
Mistake 2: Assuming “it compiled, so it’s correct” and skipping go vet
A missing fmt.Printf argument compiles cleanly, because format strings are ordinary string literals to the type checker:
name := "Alice"
fmt.Printf("Name: %s, Age: %d\n", name)
This prints Name: Alice, Age: %!d(MISSING) at run time instead of failing to build — the second verb has no matching argument. go vet reports Printf format %d reads arg #2, but call has 1 arg for exactly this line, well before the code ships. The fix is simply to supply the missing argument:
name, age := "Alice", 30
fmt.Printf("Name: %s, Age: %d\n", name, age)
Mistake 3: Malformed struct tags
Struct tags are just backtick-delimited string literals, so a typo in one is invisible to the compiler:
type User struct {
Name string `json="name"`
}
The colon between key and value is missing, so encoding/json will silently fail to match this tag and fall back to using the field name instead. go vet’s structtag analyzer catches the malformed syntax directly, reporting that the tag is not compatible with reflect.StructTag.Get. The corrected tag uses a colon between the key and the quoted value:
type User struct {
Name string `json:"name"`
}
Best Practices
- Configure your editor to run
gofmt -w(or the import-awaregoimports) automatically on save, so you never think about formatting by hand. - Add a CI step that runs
gofmt -l .and fails the build if it prints any file names — that means someone forgot to format before committing. - Run
go vet ./...in CI as its own step, separate fromgo test, so a vet-only failure is easy to spot in the log. - Never suppress a go vet finding by rewriting code to merely look different to the analyzer — fix the underlying issue, whether that’s the wrong verb, the value receiver, or the malformed tag.
- Don’t argue with gofmt’s output — there is no configuration file, and that lack of configuration is exactly what keeps every Go codebase looking the same.
- For larger projects, layer
golangci-linton top of go vet; it bundles go vet with many additional linters behind one command. - Treat a go vet failure with the same seriousness as a compiler error during code review — it means the code does something other than what it appears to do.
Practice Exercises
- Write a small Go file with inconsistent spacing around an
ifstatement and a multi-linevarblock, then predict exactly whatgofmt -dwill show as the diff before you run it. - Write a program that calls
fmt.Printfwith a%sverb but passes anintargument. Run it and observe the runtime output, then rungo vetand compare its message to what actually happened. - Define a struct that embeds a
sync.WaitGroupand write a method with a value receiver that callsAddon it. Rungo vetand explain, in your own words, why copying async.WaitGroupis just as dangerous as copying async.Mutex.
Summary
gofmtparses your code into an AST and reprints it in one fixed, non-configurable style, so Go code never has formatting-only diffs.go fmt ./...is the everyday wrapper around gofmt that formats a whole module in place.go vettype-checks your package and then runs independent analyzers that look for legal-but-almost-certainly-wrong patterns: bad Printf verbs, malformed struct tags, copied locks, unreachable code, and more.- Neither tool changes runtime behavior by itself — gofmt only touches layout, and go vet only reports; a human still fixes the underlying bug.
go testruns a subset of go vet automatically, but running the fullgo vet ./...andgofmt -l .in CI catches issues thatgo testalone would miss.
