Skip to content
C Codeloom
Go

Go Tooling: go vet, gofmt, and go test

A practical guide to Go's built-in tooling trio: gofmt for formatting, go vet for static checks, and go test for unit, benchmark, and coverage workflows.

·5 min read · By Codeloom
Intermediate 8 min read

What you'll learn

  • How gofmt enforces a single canonical style
  • What go vet catches that the compiler does not
  • Writing table-driven tests with go test
  • Benchmarks, coverage, and race detection
  • Wiring the trio into pre-commit and CI

Prerequisites

  • Familiar with shell
  • Basic Go syntax and packages

What and Why

Go ships with an opinionated toolchain that handles most of what other ecosystems leave to third-party packages. Three commands cover formatting, static analysis, and testing:

  • gofmt (and its superset goimports) enforces canonical formatting,
  • go vet reports suspicious constructs the compiler accepts,
  • go test runs unit tests, benchmarks, and coverage analysis.

Together they cover the everyday quality loop without YAML configs or plugin matrices.

Mental Model

Think of each tool as a layer in a pipeline. The compiler ensures your code is syntactically and type-correct. gofmt ensures it looks the same as everyone else’s. go vet flags semantically suspicious patterns that compile but probably misbehave. go test proves runtime behavior. Failures at any layer should block a commit.

Because the rules are baked into the toolchain, there is no debate over braces or trailing commas. You spend code review on logic, not whitespace.

Hands-on Example

Consider a small package calc with a buggy printf call and missing tests.

// calc/calc.go
package calc

import "fmt"

func Add(a, b int) int { return a + b }

func Describe(a, b int) string {
    // Bug: %d expects int but we pass a string
    return fmt.Sprintf("sum of %d and %d is %s", a, b, Add(a, b))
}

Run the trio:

$ gofmt -l ./...
calc/calc.go            # listed means needs reformatting
$ gofmt -w ./...        # rewrite in place

$ go vet ./...
calc/calc.go:9:9: fmt.Sprintf format %s has arg Add(a, b) of wrong type int

go vet caught the format string mismatch the compiler ignored. Fix it:

return fmt.Sprintf("sum of %d and %d is %d", a, b, Add(a, b))

Now add a table-driven test:

// calc/calc_test.go
package calc

import "testing"

func TestAdd(t *testing.T) {
    cases := []struct {
        name string
        a, b int
        want int
    }{
        {"zeros", 0, 0, 0},
        {"positive", 2, 3, 5},
        {"negative", -4, 1, -3},
    }
    for _, tc := range cases {
        t.Run(tc.name, func(t *testing.T) {
            if got := Add(tc.a, tc.b); got != tc.want {
                t.Errorf("Add(%d,%d)=%d, want %d", tc.a, tc.b, got, tc.want)
            }
        })
    }
}

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = Add(123, 456)
    }
}

Run tests with coverage and the race detector:

$ go test ./... -race -cover
ok  example.com/calc  0.812s  coverage: 100.0% of statements

$ go test ./calc -bench=. -benchmem
BenchmarkAdd-10  1000000000  0.31 ns/op  0 B/op  0 allocs/op
  edit code
   |
   v
gofmt -w  --> canonical layout
   |
   v
go vet    --> suspicious constructs
   |
   v
go test   --> behavior + coverage + race
   |
   v
commit / push
The Go quality loop: format, vet, and test on every change

Common Pitfalls

  • Skipping gofmt in editors. Some IDEs only format on save for the current file. Run gofmt -l ./... in CI; a non-empty list should fail the build.
  • Assuming go vet is a linter. It is narrow on purpose: only high-confidence checks. For broader linting use staticcheck or golangci-lint, but treat them as complements, not replacements.
  • Forgetting -race. Race detection is off by default and finds bugs no other check will. Always run race in CI.
  • Untestable code. Tests that depend on time, files, or network drift become flaky. Inject dependencies via interfaces; a tiny Clock interface beats time.Now() sprinkled everywhere.
  • Coverage as a goal. Chasing 100% coverage encourages tests that exercise lines without asserting behavior. Use coverage to find gaps, not to grade quality.

Practical Tips

  • Add a Makefile or task target: check: gofmt -l . && go vet ./... && go test -race ./.... One command, every time.
  • Use goimports -w to auto-add and reorder imports as you save.
  • Mark slow tests with if testing.Short() { t.Skip() } and run go test -short locally for quick loops.
  • Capture failing inputs with t.Logf instead of fmt.Println. The output is suppressed on success and grouped per test on failure.
  • For table-driven tests, always wrap cases in t.Run(tc.name, ...). Sub-test names show up in failure output and allow go test -run TestAdd/negative to target a single case.

Wrap-up

Go’s toolchain pushes formatting, static checks, and testing into a frictionless default workflow. Use gofmt to end style debates, go vet as a cheap second opinion the compiler can’t give, and go test with race and coverage to keep behavior honest. Wire all three into pre-commit and CI, and quality becomes a property of the build, not a discipline you have to remember.