Build & Dev Tools
Go ships with excellent tooling, and the community fills the gaps. Static analysis catches bugs before tests run. Code generation eliminates boilerplate. Build tags enable conditional compilation. Live reload speeds up development. This topic covers the tools you should have in every Go project.
go vet: Catch Common Mistakes
go vet ships with Go and catches mistakes the compiler misses:
go vet ./...
What it catches:
// Printf format mismatch
fmt.Printf("name: %d", name) // go vet: Printf format %d has arg name of wrong type string
// Unreachable code
func example() int {
return 42
fmt.Println("never runs") // go vet: unreachable code
}
// Copying a lock
var mu sync.Mutex
mu2 := mu // go vet: assignment copies lock value
// Struct tag errors
type User struct {
Name string `json:name` // go vet: struct field tag `json:name` not compatible
}
Run go vet in CI. It has zero false positives and catches real bugs.
staticcheck: Advanced Static Analysis
staticcheck is the most important third-party analysis tool:
go install honnef.co/go/tools/cmd/staticcheck@latest
staticcheck ./...
It catches more than go vet:
// Deprecated function usage
strings.Title("hello") // SA1019: strings.Title is deprecated
// Inefficient string conversion
string(n) // where n is an int -- SA1019: use strconv.Itoa
// Useless assignments
x := doSomething()
x = doSomethingElse() // SA4006: x is overwritten before being used
// Empty branches
if err != nil {
// SA9003: empty branch
}
staticcheck also enforces style rules and suggests simplifications.
golangci-lint: Meta-Linter
golangci-lint runs many linters in parallel:
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
golangci-lint run ./...
Configure it with .golangci.yml:
linters:
enable:
- errcheck # check error return values
- govet # go vet
- staticcheck # advanced analysis
- unused # find unused code
- gosimple # simplification suggestions
- ineffassign # detect useless assignments
- gocritic # opinionated suggestions
- revive # golint replacement
linters-settings:
errcheck:
check-type-assertions: true
gocritic:
enabled-tags:
- diagnostic
- performance
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0
Use golangci-lint in CI. It is the standard lint step for Go projects:
# GitHub Actions
- name: Lint
uses: golangci/golangci-lint-action@v6
with:
version: latest
go generate: Code Generation
go generate runs commands embedded in Go source files:
//go:generate stringer -type=Status
//go:generate mockgen -source=store.go -destination=mock_store.go
package mypackage
go generate ./...
Common uses:
- stringer: Generate
String()methods for enum types - mockgen: Generate mock implementations for testing
- sqlc: Generate type-safe Go from SQL queries
- protoc-gen-go: Generate Go from Protocol Buffers
The convention is to commit generated files. CI should verify that go generate produces no diff:
go generate ./...
git diff --exit-code
Build Tags for Conditional Compilation
Build tags control which files are included in a build:
//go:build linux
package mypackage
// This file is only compiled on Linux
//go:build integration
package store_test
// This file is only compiled with: go test -tags integration
Combine tags with boolean logic:
//go:build (linux || darwin) && !race
Common uses:
- Separate integration tests from unit tests
- Platform-specific implementations
- Debug-only code
- Feature flags
ldflags: Embedding Version Info
Inject values at build time without modifying source code:
var version = "dev"
func main() {
fmt.Println("version:", version)
}
go build -ldflags "-X main.version=1.2.3" -o myapp
Multiple flags:
go build -ldflags "\
-X main.version=$(git describe --tags) \
-X main.commit=$(git rev-parse --short HEAD) \
-X main.buildDate=$(date -u +%Y-%m-%d) \
-s -w" \
-o myapp
-s strips the symbol table, -w strips DWARF debugging info. Both reduce binary size.
Air: Live Reload in Development
air watches your source files and rebuilds on change:
go install github.com/air-verse/air@latest
air
Configure with .air.toml:
[build]
cmd = "go build -o ./tmp/main ./cmd/api"
bin = "./tmp/main"
delay = 1000 # ms
[build.exclude_dir]
values = ["tmp", "vendor", "node_modules"]
[build.include_ext]
values = ["go", "html", "css"]
[log]
time = true
Now every time you save a .go file, air rebuilds and restarts your server. This is essential for web development in Go.
Delve: Debugging
Delve is Go's debugger. It understands goroutines, channels, and Go's runtime:
go install github.com/go-delve/delve/cmd/dlv@latest
Debug a Program
dlv debug ./cmd/myapp
(dlv) break main.go:42
(dlv) continue
(dlv) print myVar
(dlv) next
(dlv) step
(dlv) goroutines
(dlv) goroutine 1 bt # backtrace of goroutine 1
Debug a Test
dlv test ./internal/store -- -test.run TestCreate
Attach to a Running Process
dlv attach <pid>
VS Code Integration
Delve integrates with VS Code through the Go extension. Set breakpoints in the editor and debug visually.
Key Delve commands:
Command Description
----------------------------
break (b) set breakpoint
continue (c) run until breakpoint
next (n) step over
step (s) step into
print (p) print variable
locals show local variables
goroutines list goroutines
bt backtrace
The CI Pipeline
A standard Go CI pipeline:
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
- name: Vet
run: go vet ./...
- name: Lint
uses: golangci/golangci-lint-action@v6
- name: Test
run: go test -race -coverprofile=coverage.out ./...
- name: Build
run: go build ./...
The order matters: vet and lint are fast and catch issues before expensive test runs.
Common Pitfalls
- Not running go vet in CI.
go vethas zero false positives. There is no reason to skip it. - Using too many linters. Start with the defaults in
golangci-lint. Enable more only when you understand what they check. - Not committing generated files. If generated files are not in the repo, every developer needs the generation tools. Commit them and verify in CI.
- Using fmt.Println for debugging instead of Delve. Print debugging works, but Delve lets you inspect state, goroutines, and channels interactively.
- Skipping the -race flag in tests.
go test -racefinds data races. Run it in CI for every test. - Not using Air during development. Manual rebuild-restart cycles waste minutes every day. Live reload pays for itself immediately.
Key Takeaways
go vetcatches real bugs with zero false positives. Run it always.staticcheckcatches whatgo vetmisses. It is the most valuable single linter.golangci-lintruns many linters in parallel. Configure it once, run in CI.go generateautomates code generation. Commit generated files and verify in CI.- Build tags separate integration tests, platform code, and feature flags.
-ldflagsembeds version info at build time without modifying source.airprovides live reload for development.delveprovides interactive debugging.- A CI pipeline should run:
vet->lint->test -race->build.