The go command explained: run, build, test, mod, fmt and vet
A practical guide to the essential Go commands: go run, build, test, fmt, vet and mod. Everything you need to work locally and in CI.

When you start with Go coming from Java, Python or Node, one of the first things you notice is that you don’t need Maven, Gradle, pytest, black or ESLint. The go binary that ships with the language already includes a compiler, dependency manager, test runner, formatter, linter and code generator. All in a single command.
This is not an accident. It is a deliberate design decision. The Go team bet from the beginning on standard tools integrated into the toolchain itself. Less configuration, fewer debates about which tool to use, less friction when onboarding someone new to the project. If you know Go, you already know how to compile, test and format any Go project.
This article covers the subcommands you will use every day: go run, go build, go test, go fmt, go vet, go mod, go generate, go install and go env. With practical examples, useful flags and how to combine them in a CI pipeline.
go run: compile and execute in one step
go run compiles and executes a Go program without generating a persistent binary. It is the equivalent of python script.py or node app.js: useful for rapid development and scripts.
go run main.goIf your main imports other files from the same package, you can pass multiple files or use the directory pattern:
go run .
go run ./cmd/serverPassing arguments to the program
Arguments after the file or package are passed directly to the program:
go run main.go --port 8080 --env productionUseful go run flags
# Show the commands it runs internally
go run -x main.go
# Compile without optimizations (useful for debugging with Delve)
go run -gcflags="-N -l" main.goWhen NOT to use go run
go run compiles every time you execute it. There is no implicit cache of the resulting binary. For a server you restart 50 times a day, that is acceptable. For a binary you are going to distribute or deploy, you need go build.
go runis for local development. Never use it in production or in a final Dockerfile.
go build: creating binaries
go build compiles your code and generates an executable binary. If you do not specify an output name, it uses the module or directory name.
go build -o server ./cmd/serverThe resulting binary is static by default (in most cases): it needs no runtime, it does not require Go to be installed on the target machine. Copy the binary and it works.
Cross-compilation
One of the best features of Go is cross-compilation. You can generate binaries for any platform from your machine:
# Linux AMD64 (the typical choice for servers and Docker containers)
GOOS=linux GOARCH=amd64 go build -o server-linux ./cmd/server
# macOS ARM (Apple Silicon)
GOOS=darwin GOARCH=arm64 go build -o server-mac ./cmd/server
# Windows
GOOS=windows GOARCH=amd64 go build -o server.exe ./cmd/serverYou don’t need a virtual machine, you don’t need Docker, you don’t need a special CI. Just two environment variables. This is something that in Java or Python simply does not exist natively.
Common build flags
# Reduce binary size by removing debug information
go build -ldflags="-s -w" -o server ./cmd/server
# Inject variables at compile time (version, commit, date)
go build -ldflags="-X main.version=1.2.3 -X main.commit=$(git rev-parse HEAD)" -o server ./cmd/server
# Fully static build (no CGO dependencies)
CGO_ENABLED=0 go build -o server ./cmd/server
# See what commands it runs internally
go build -x -o server ./cmd/serverInjecting version at compile time
A very common pattern is to define variables in your main.go and fill them with -ldflags:
package main
import "fmt"
var (
version = "dev"
commit = "none"
date = "unknown"
)
func main() {
fmt.Printf("server %s (commit: %s, built: %s)\n", version, commit, date)
}go build -ldflags="-X main.version=1.2.3 -X main.commit=$(git rev-parse --short HEAD) -X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" -o server .This gives you a binary that knows exactly which version it is and when it was compiled. Very useful for logs and for diagnosing issues in production.
go test: tests, coverage and benchmarks
go test is the built-in test runner. It reads *_test.go files, executes functions that start with Test, Benchmark or Example, and reports results. No JUnit, no pytest, no configuration.
# Run tests in the current package
go test
# Run tests across the entire project
go test ./...
# With verbose output
go test -v ./...
# Run only tests matching a pattern
go test -run TestCreateUser ./internal/user/If you want to go deeper into testing, I have a dedicated article on testing in Go with more detail on table-driven tests, mocks and organization.
Coverage
# See coverage percentage
go test -cover ./...
# Generate a coverage file for detailed inspection
go test -coverprofile=coverage.out ./...
# View coverage line by line in the browser
go tool cover -html=coverage.out
# View coverage per function
go tool cover -func=coverage.outThe output of -cover gives you a percentage per package:
ok github.com/user/project/internal/user 0.012s coverage: 87.3% of statements
ok github.com/user/project/internal/auth 0.008s coverage: 92.1% of statementsBenchmarks
Go has native support for benchmarks. You define Benchmark* functions in your test files:
func BenchmarkParseConfig(b *testing.B) {
data := loadTestConfig()
b.ResetTimer()
for i := 0; i < b.N; i++ {
ParseConfig(data)
}
}And you run them with:
# Run benchmarks
go test -bench=. ./...
# Benchmarks with memory information
go test -bench=. -benchmem ./...
# Only benchmarks, no regular tests
go test -bench=. -run=^$ ./...
# Run a specific benchmark
go test -bench=BenchmarkParseConfig ./internal/config/Typical output:
BenchmarkParseConfig-8 1000000 1052 ns/op 256 B/op 4 allocs/opThat tells you: it ran one million times, each run took 1052 nanoseconds, allocated 256 bytes and performed 4 allocations. Concrete information for optimization.
Race detector
Go has a built-in race condition detector. Enable it with -race:
go test -race ./...This instruments the code to detect concurrent accesses to shared memory. It is slower, so do not use it in benchmarks, but it should be mandatory in CI.
Useful go test flags
# Global timeout (default 10 minutes)
go test -timeout 30s ./...
# Run tests in parallel (default GOMAXPROCS)
go test -parallel 4 ./...
# Disable test cache (forces re-execution)
go test -count=1 ./...
# Short tests (skip heavy tests marked with testing.Short())
go test -short ./...go fmt: the end of formatting debates
go fmt formats your code according to the official Go style. There is no configuration, no options, no .editorconfig or .prettierrc. One format. For everyone.
# Format a file
go fmt main.go
# Format the entire project
go fmt ./...In practice, most projects use gofmt (the underlying formatter) or goimports (which also organizes imports):
# goimports: formats + organizes imports + adds missing imports
goimports -w .Why this matters
In Java you have Checkstyle, SpotBugs, Google Java Format, IntelliJ formatter, each with its own config. In Python you have Black, YAPF, autopep8, isort. In JavaScript you have Prettier, ESLint, Standard. Each project picks one, configures it, and there is always someone whose IDE is set up differently and introduces formatting changes in diffs.
In Go that problem does not exist. go fmt is the standard. Period. No debate over tabs vs spaces (tabs), no debate over where the opening brace goes (same line), no debate over maximum line width (there is no forced limit). The format is part of the language.
If your code is not formatted with
go fmt, the community considers it wrong. It is that simple.
go vet: built-in static analysis
go vet examines your code looking for common mistakes that the compiler does not detect: incorrectly passed arguments to fmt.Printf, loop variables captured in goroutines, impossible conditions, unreachable code and more.
# Analyze the current package
go vet
# Analyze the entire project
go vet ./...What go vet detects
Some examples of what it finds:
// Printf with incorrect arguments
fmt.Printf("user: %d", username) // vet: wrong type for %d
// Copying a sync.Mutex (serious concurrency error)
var mu sync.Mutex
mu2 := mu // vet: assignment copies lock value
// Impossible comparison
if x != x { // vet: suspicious comparison
}
// Unreachable code
func foo() int {
return 1
fmt.Println("never") // vet: unreachable code
}go vet is not a full linter like golangci-lint, but it covers the most dangerous errors and is fast. In CI it should always run.
go vet vs golangci-lint
go vet is a subset. golangci-lint bundles dozens of linters (including vet) and allows you to configure rules. For a serious project, use both:
# In CI: start with the fast, standard check
go vet ./...
# Then the full analysis
golangci-lint rungo mod: dependency management
go mod is Go’s module system. It manages your project’s dependencies through the go.mod file. If you come from other languages: go.mod is your pom.xml, package.json or requirements.txt.
For a complete guide on modules, see the Go modules article. Here I cover the subcommands you will use every day.
go mod init
Initializes a new module:
go mod init github.com/user/projectThis creates a go.mod with the module name and Go version:
module github.com/user/project
go 1.22go mod tidy
The command you will use the most. It analyzes your imports, adds missing dependencies to go.mod, and removes unused ones:
go mod tidyRun it after adding or removing imports. In CI, a common technique is to verify that go.mod and go.sum are up to date:
go mod tidy
git diff --exit-code go.mod go.sumIf there are differences, someone forgot to run go mod tidy before pushing.
go mod download
Downloads all dependencies to the local cache without compiling anything:
go mod downloadUseful in Dockerfiles to leverage layer caching:
FROM golang:1.22-alpine AS builder
WORKDIR /app
# First copy only go.mod and go.sum
COPY go.mod go.sum ./
RUN go mod download
# Then the code (this layer is invalidated more often)
COPY . .
RUN go build -o server ./cmd/servergo mod vendor
Copies all dependencies into a vendor/ directory inside the project:
go mod vendorThis allows builds without internet access and guarantees reproducibility. To compile using the vendor:
go build -mod=vendor -o server ./cmd/servergo mod graph and go mod why
For debugging dependencies:
# View the full dependency graph
go mod graph
# Find out why a dependency is in your go.mod
go mod why github.com/lib/pqgo mod why is especially useful when you see a dependency in go.sum and don’t know who brought it in.
go generate: code generation
go generate runs commands defined in special comments inside your Go code. It is not a build system or a preprocessor: it is a mechanism to run tools that generate Go code.
//go:generate stringer -type=Status
//go:generate mockgen -source=repository.go -destination=mock_repository.go
//go:generate protoc --go_out=. --go-grpc_out=. api.protoTo run all generators in the project:
go generate ./...Common use cases
- Enums with stringer: Generates
String()methods for types based oniota. - Mocks with mockgen: Generates mock implementations of interfaces for tests.
- Protocol Buffers: Generates Go code from
.protofiles. - SQL embeds: Tools like
sqlcgenerate type-safe Go code from SQL queries.
Best practices with go generate
- Commit the generated code. Whoever clones your repo should not need to have
protoc,mockgenorstringerinstalled to compile. - Verify in CI that the generated code is up to date:
go generate ./...
git diff --exit-codeIf there are differences, someone modified the source code without regenerating.
go install: installing tools
go install compiles and installs a binary to $GOPATH/bin (or $GOBIN if you have it set). It is the standard way to install tools written in Go.
# Install a specific tool with a version
go install golang.org/x/tools/cmd/goimports@latest
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.59.0
# Install from the current project
go install ./cmd/serverDifference between go install and go build
go buildgenerates the binary in the current directory (or wherever you specify with-o).go installgenerates the binary in$GOPATH/bin.
For CLI tools you want available globally, use go install. For your project, use go build.
Managing tool versions in the project
A common pattern is to have a tools.go file with a build tag that is never compiled, only so that go mod tidy records the tool dependencies:
//go:build tools
package tools
import (
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
_ "go.uber.org/mock/mockgen"
_ "golang.org/x/tools/cmd/goimports"
)This way the versions are pinned in go.mod and the whole team uses the same ones.
go env: understanding your environment
go env shows all the environment variables that affect the Go toolchain. It is the first command to run when something is not working as expected.
# View all variables
go env
# View a specific variable
go env GOPATH
go env GOROOT
go env GOOS
go env GOARCH
# View in JSON format
go env -jsonImportant variables
| Variable | What it does |
|---|---|
GOPATH | Base directory for dependencies and installed binaries |
GOROOT | Go installation directory |
GOBIN | Where binaries are installed with go install |
GOOS | Target operating system for compilation |
GOARCH | Target architecture for compilation |
GOPROXY | Proxy for downloading modules (default https://proxy.golang.org) |
GONOSUMCHECK | Modules that are not verified in the sumdb |
CGO_ENABLED | Whether compiling C code is allowed (0 or 1) |
GOFLAGS | Flags applied to all go commands |
Modifying variables persistently
# Change the proxy (useful in corporate environments)
go env -w GOPROXY=https://goproxy.io,direct
# Disable CGO by default
go env -w CGO_ENABLED=0These settings are saved in $GOPATH/env and persist between sessions.
Combining commands in CI/CD
A CI pipeline for a typical Go project looks like this:
#!/bin/bash
set -euo pipefail
echo "=== Checking formatting ==="
gofmt -l . | tee /tmp/fmt-check
if [ -s /tmp/fmt-check ]; then
echo "ERROR: unformatted files"
exit 1
fi
echo "=== Static analysis ==="
go vet ./...
echo "=== Checking go.mod ==="
go mod tidy
git diff --exit-code go.mod go.sum
echo "=== Checking generated code ==="
go generate ./...
git diff --exit-code
echo "=== Tests with race detector ==="
go test -race -cover -coverprofile=coverage.out ./...
echo "=== Coverage ==="
go tool cover -func=coverage.out
echo "=== Build ==="
CGO_ENABLED=0 go build -ldflags="-s -w" -o /tmp/app ./cmd/serverExample with GitHub Actions
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Verify formatting
run: |
if [ -n "$(gofmt -l .)" ]; then
echo "Code not formatted:"
gofmt -l .
exit 1
fi
- name: Vet
run: go vet ./...
- name: Test
run: go test -race -coverprofile=coverage.out ./...
- name: Build
run: CGO_ENABLED=0 go build -ldflags="-s -w" -o app ./cmd/serverNotice the order: formatting first (it is instant and fails fast), then vet, then tests, then build. No Maven taking 30 seconds to start up, no npm install downloading 200 MB. A medium-sized Go project passes CI in under a minute.
Comparison table: Go tooling vs other ecosystems
| Task | Go | Java | Python | Node.js |
|---|---|---|---|---|
| Compile | go build | mvn package / gradle build | N/A (interpreted) | N/A (interpreted) |
| Run | go run | java -jar / mvn exec:java | python script.py | node app.js |
| Tests | go test | JUnit + Maven/Gradle | pytest / unittest | Jest / Vitest |
| Format | go fmt | google-java-format / Spotless | Black / YAPF | Prettier |
| Linter | go vet + golangci-lint | Checkstyle / SpotBugs | Ruff / Flake8 / Pylint | ESLint |
| Dependencies | go mod | Maven / Gradle | pip / Poetry / uv | npm / pnpm / yarn |
| Coverage | go test -cover | JaCoCo | coverage.py / pytest-cov | c8 / istanbul |
| Benchmarks | go test -bench | JMH | pytest-benchmark | Benchmark.js |
| Cross-compile | GOOS=x GOARCH=y go build | GraalVM native-image (limited) | Not native | Not native (pkg) |
| Code generation | go generate | Annotation processors | Not standard | Not standard |
The main difference: in Go everything is a single binary with a consistent interface. In other ecosystems you need to install, configure and maintain separate tools for each task.
What makes Go’s tooling different
The go command is not just a compiler. It is a statement about how a language’s tooling should work. Formatting without configuration. Tests without an external framework. Cross-compilation with two environment variables. Dependency management without a separate lock file (the go.sum is auto-generated).
There are things it does not cover: advanced linting (you need golangci-lint), hot reload (you need air or similar), and release management (you need goreleaser or scripts). But the foundation the standard toolchain offers is more complete than that of any other language I have used.
If you are getting started with Go, spend an hour exploring go help and the subcommands we have covered. That hour will save you days of tool configuration that in other ecosystems you take for granted as necessary. If you want a guide to take your first steps, start with getting started with Go.


