Skip to content

Table of Contents

Tooling & Modules

Contents

Modules

A module is a tree of packages with a go.mod at its root defining the module path and dependencies.

go mod init github.com/you/project   # create go.mod

go.mod example:

module github.com/you/project

go 1.22

require (
    github.com/stretchr/testify v1.9.0
    golang.org/x/sync v0.7.0
)

require github.com/davecgh/go-spew v1.1.1 // indirect

replace github.com/foo/bar => ../bar      // local override
exclude github.com/bad/dep v1.2.3

go.sum holds cryptographic checksums — commit it. GOFLAGS=-mod=readonly keeps CI honest.

go mod commands

go mod init <path>        # initialize a new module
go mod tidy               # add missing + remove unused deps; sync go.sum
go mod download           # download modules to the cache
go mod verify             # check cached modules match go.sum
go mod why <pkg>          # explain why a dependency is needed
go mod graph              # print the module dependency graph
go mod edit -require=x@v1.2.3   # programmatically edit go.mod
go mod vendor             # write deps into ./vendor (then build -mod=vendor)

go get example.com/pkg@latest    # add/upgrade a dependency
go get example.com/pkg@v1.2.3    # specific version
go get example.com/pkg@none      # remove a dependency
go get -u ./...                  # upgrade all deps to latest minor/patch
go get -u=patch ./...            # patch-level upgrades only

go list -m all                   # list all modules in the build
go list -m -versions example.com/pkg  # available versions
go work init ./a ./b             # multi-module workspace (go.work)
go work use ./c

go mod tidy is the one you run most: it makes go.mod/go.sum exactly reflect imports.

Build, run, install

go run .                    # compile + run current package
go run ./cmd/server         # run a specific main package
go run main.go              # run a file

go build ./...              # compile everything (no output for non-main)
go build -o bin/app ./cmd/app    # named output binary
go build -ldflags="-s -w"        # strip debug info -> smaller binary
go build -ldflags="-X main.version=$(git describe)"  # inject a variable
go build -race ./...             # with race detector
go build -trimpath               # remove local paths from binary (reproducible)

go install ./cmd/app        # build + install to $GOBIN (or $GOPATH/bin)
go install example.com/cmd/tool@latest  # install a remote tool

go clean -cache             # clear build cache
go env GOPATH GOBIN GOMODCACHE   # inspect environment

Vet & format

gofmt -w .                  # format files in place (canonical formatting)
gofmt -l .                  # list files that need formatting (CI gate)
go fmt ./...                # gofmt over packages
goimports -w .              # gofmt + manage import grouping/removal (separate tool)

go vet ./...                # static checks: printf args, lock copies, etc.
go vet -vettool=$(which shadow) ./...   # extra analyzers

gofmt is non-negotiable in Go; there is one true style. go vet catches real bugs (e.g. Printf verb mismatches, struct-tag typos, unreachable code).

Build tags

Conditional compilation via //go:build constraints (must be near the top, above package, followed by a blank line).

//go:build linux && amd64

package foo
//go:build integration

package foo
// run with: go test -tags=integration

Common forms: //go:build linux, //go:build !windows, //go:build (darwin || linux) && cgo, //go:build ignore (excluded from normal builds).

Filename suffixes also constrain builds: file_linux.go, file_amd64.go, file_linux_arm64.go, file_test.go.

Other useful directives:

//go:embed templates/*.html    // embed files into the binary (needs import "embed")
//go:generate stringer -type=Color   // ran by `go generate ./...`
//go:noinline

Cross-compilation

Set GOOS and GOARCH. No C toolchain needed if CGO_ENABLED=0.

GOOS=linux   GOARCH=amd64 go build -o app-linux   ./cmd/app
GOOS=darwin  GOARCH=arm64 go build -o app-mac-m1  ./cmd/app
GOOS=windows GOARCH=amd64 go build -o app.exe     ./cmd/app
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build ./... # static, for scratch containers

go tool dist list           # all supported GOOS/GOARCH pairs
go env GOOS GOARCH          # current target

Common GOARCH: amd64, arm64, 386, arm. Common GOOS: linux, darwin, windows, freebsd, js (with GOARCH=wasm), wasip1.

Common test flags

go test ./...                    # all packages
go test -v ./pkg                 # verbose
go test -run 'TestFoo/sub'       # regexp filter on test+subtest name
go test -count=1                 # bypass test cache
go test -race                    # data race detector
go test -cover                   # coverage %
go test -coverprofile=c.out -covermode=atomic ./...
go test -bench=. -benchmem       # benchmarks with allocs
go test -benchtime=3s -count=5   # tune benchmark runs
go test -fuzz=FuzzX -fuzztime=30s
go test -timeout=30s             # per-package test timeout (default 10m)
go test -short                   # skip tests guarded by testing.Short()
go test -shuffle=on              # randomize test order
go test -parallel=4              # max parallel tests
go test -tags=integration        # build tag selection
go test -json ./... | gotestsum  # machine-readable output
go test -failfast                # stop after first failure

pprof quickstart

import _ "net/http/pprof"        // registers handlers on the default mux

go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }()
# Collect & explore live profiles:
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30   # CPU
go tool pprof http://localhost:6060/debug/pprof/heap                 # memory
go tool pprof http://localhost:6060/debug/pprof/goroutine            # goroutines

# From benchmarks:
go test -bench=. -cpuprofile=cpu.out -memprofile=mem.out
go tool pprof cpu.out
# Inside pprof: top, list <func>, web (needs graphviz), peek, traces

# Flame graph in browser:
go tool pprof -http=:8081 cpu.out

# Execution tracer:
go test -trace=trace.out
go tool trace trace.out

golangci-lint

Aggregates many linters into one fast run. (Third-party, the de facto standard.)

golangci-lint run ./...
golangci-lint run --fix          # auto-fix where possible

.golangci.yml:

linters:
  enable:
    - errcheck
    - govet
    - staticcheck
    - revive
    - gosimple
    - ineffassign
    - unused
    - gofmt
    - goimports
linters-settings:
  govet:
    enable: [shadow]
run:
  timeout: 5m
issues:
  exclude-rules:
    - path: _test\.go
      linters: [errcheck]

Makefile snippet

BINARY := app
PKG := ./...
VERSION := $(shell git describe --tags --always --dirty)
LDFLAGS := -s -w -X main.version=$(VERSION)

.PHONY: all build test lint fmt vet tidy clean run cover

all: lint test build

build:
    CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -o bin/$(BINARY) ./cmd/$(BINARY)

run:
    go run ./cmd/$(BINARY)

test:
    go test -race -count=1 $(PKG)

cover:
    go test -coverprofile=cover.out $(PKG) && go tool cover -html=cover.out

bench:
    go test -bench=. -benchmem $(PKG)

lint:
    golangci-lint run $(PKG)

fmt:
    gofmt -w . && goimports -w .

vet:
    go vet $(PKG)

tidy:
    go mod tidy

clean:
    rm -rf bin cover.out
    go clean -cache

Tabs (not spaces) indent Makefile recipes.