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/printer package, 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 -l it reports a mismatch, with -w it overwrites the file, and with no flags it just prints the result.

go vet:

  • Load the target package(s) with the same machinery go build uses, 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 %d verb paired with a non-integer argument, or a struct literal containing a sync.Mutex being 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-aware goimports) 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 from go 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-lint on 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

  1. Write a small Go file with inconsistent spacing around an if statement and a multi-line var block, then predict exactly what gofmt -d will show as the diff before you run it.
  2. Write a program that calls fmt.Printf with a %s verb but passes an int argument. Run it and observe the runtime output, then run go vet and compare its message to what actually happened.
  3. Define a struct that embeds a sync.WaitGroup and write a method with a value receiver that calls Add on it. Run go vet and explain, in your own words, why copying a sync.WaitGroup is just as dangerous as copying a sync.Mutex.

Summary

  • gofmt parses 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 vet type-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 test runs a subset of go vet automatically, but running the full go vet ./... and gofmt -l . in CI catches issues that go test alone would miss.