The go Command, Builds and Cross-Compilation
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Go 1.27 (go1.27.2), checked on 2026-10-18. The go command is the only tool most Go
programmers ever need to build, run, install and ship a program: it finds packages, decides which files belong to a
build, compiles them with the toolchain, caches the result and links one executable. The
Getting Started page introduced go run, go build and go env,
and Modules and Dependency Management covers
go get, go mod and the module cache. This page goes deeper into everything between source and binary: the build
flags, stamping a version into a binary, what the build records about itself, the caches, build constraints, build
modes, linking, and cross-compiling to other operating systems and processors.
The running example
Every command below runs against a small module in the style of the shelf scenario: a command with a version
variable that the linker can overwrite, and an internal package with operating-system specific files.
shelf/
go.mod
cmd/shelf/main.go
internal/platform/platform.go
internal/platform/lock_linux.go
internal/platform/lock_darwin.go
internal/platform/lock_windows.go
internal/platform/lock_other.go
internal/platform/debug_on.go
internal/platform/debug_off.go
module example.com/shelf
go 1.27
package main
import (
"fmt"
"runtime"
"runtime/debug"
"example.com/shelf/internal/platform"
)
// version is overridden at link time with -ldflags "-X main.version=...".
var version = "dev"
func main() {
fmt.Println("shelf", version)
fmt.Println("os/arch:", runtime.GOOS, runtime.GOARCH)
fmt.Println("lock dir:", platform.LockDir())
fmt.Println("debug:", platform.Debug)
if bi, ok := debug.ReadBuildInfo(); ok {
for _, s := range bi.Settings {
if s.Key == "vcs.revision" || s.Key == "vcs.modified" || s.Key == "-tags" || s.Key == "-trimpath" {
fmt.Println(s.Key, "=", s.Value)
}
}
}
}
Source: runtime/debug.ReadBuildInfo and cmd/link
The platform package is a handful of tiny files; the next sections show which of them each build selects.
package platform
// LockDir returns the directory for lock files on Linux.
func LockDir() string { return "/run/shelf" }
The macOS file returns /var/run/shelf, the Windows file C:\ProgramData\shelf, and lock_other.go (guarded by a
constraint described later) /tmp/shelf. debug_on.go and debug_off.go declare const Debug = true and
const Debug = false behind the shelfdebug tag and its negation. This module passes gofmt -l and go vet.
$ go run ./cmd/shelf
shelf dev
os/arch: darwin arm64
lock dir: /var/run/shelf
debug: false
The subcommands at a glance
go help lists them all. The ones about building, and where this site documents the others:
| Command | What it does | Covered |
|---|---|---|
|
compile packages and dependencies; write the executable for a single |
this page |
|
compile a |
this page |
|
build and put executables in |
this page |
|
remove build products and caches |
this page |
|
print or change the environment |
this page and Toolchain Versions and Compatibility |
|
print information about packages and modules, optionally through a template |
this page |
|
print the toolchain version, or the build information inside a binary |
this page |
|
open a browser with a new bug report prefilled with system information |
this page |
|
run a toolchain program such as |
this page |
|
dependencies, |
Modules and Dependency Management, Workspaces, Versioning and Publishing |
|
build and run tests, benchmarks and fuzz targets |
|
|
static analysis and formatting |
|
|
run |
|
|
print documentation |
Package arguments
Most commands take package patterns. A path starting with . or .. names a directory, an import path names a
package, and ... is a wildcard. Four reserved patterns exist: std (the standard library), cmd (the toolchain’s own
commands), all (the main module’s packages and everything they import) and, since Go 1.25, work (the packages of
the work modules). Wildcard matching skips directories named testdata and those whose names begin with . or _,
and, since Go 1.25, directories named by an ignore directive in go.mod. A pattern that matches nothing is only a
warning for ... patterns but an error for a plain path.
$ go list ./...
example.com/shelf/cmd/shelf
example.com/shelf/internal/platform
$ go list std | wc -l
381
$ go list all | wc -l
61
$ go list ./nope
stat /work/shelf/nope: directory not found
go build
go build compiles the named packages and their dependencies. What happens to the result depends on what was named:
-
a single
mainpackage is linked into an executable, written to the current directory and named after the last element of the import path that is not a major-version suffix (example.com/foo/v2yieldsfoo, with.exeadded on Windows); a list of.gofiles is named after the first file; -
several packages, or one package that is not
main, are compiled and the results are discarded — the command is then only a check that everything compiles.go build ./...therefore never litters the tree with binaries; -
-ooverrides the name. If the argument names an existing directory or ends with a slash, every executable is written into it, which is the way to build several commands at once.
$ go build ./cmd/shelf # writes ./shelf
$ go build -o bin/ ./cmd/... # writes one executable per main package into bin/
$ go build -o /tmp/x ./cmd/... # with several mains and a non-directory name:
go: cannot write multiple packages to non-directory /tmp/x
The first build of a module compiles every package it needs, including the standard library; later builds reuse the
cache and compile only what changed. -v prints the name of each package as it is compiled, which makes the cache
visible:
$ go clean -cache
$ go build -v ./... 2>&1 | wc -l # cold cache: standard library plus the module's packages
59
$ go build -v ./... 2>&1 | wc -l # warm cache: nothing to compile
0
go run
go run builds the program into a temporary location, runs it with the remaining arguments, and removes it. Arguments
after the package belong to the program; flags before the package belong to the build.
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println("args:", os.Args[1:])
if len(os.Args) > 1 && os.Args[1] == "fail" {
os.Exit(3)
}
}
$ go run . a -b c
args: [a -b c]
$ go run . fail
args: [fail]
exit status 3
$ echo $?
1
Three details matter in scripts. The exit status of go run is not that of the program: any non-zero status is
reported as exit status N on standard error and go run itself exits with 1. The binary is compiled without the
debugger information that go build writes, to save time, so build explicitly to debug. And -exec prog runs the
binary through a wrapper (prog binary arguments); when GOOS or GOARCH differ from the host, go run looks on
PATH for a program called go_$GOOS_$GOARCH_exec (for example go_js_wasm_exec) and uses it to run cross-compiled
binaries in an emulator or runtime.
go install
go install is go build followed by moving the result to a standard place: executables go to GOBIN, which defaults to
$GOPATH/bin and GOPATH defaults to $HOME/go. Cross-compiled executables go to $GOBIN/$GOOS_$GOARCH subdirectories,
and go install refuses to cross-compile at all while GOBIN is explicitly set:
$ GOBIN=/work/bin GOOS=linux GOARCH=arm64 go install ./cmd/shelf
go: cannot install cross-compiled binaries when GOBIN is set
$ go install ./cmd/shelf
$ GOOS=linux GOARCH=arm64 go install ./cmd/shelf
$ GOOS=windows GOARCH=amd64 go install ./cmd/shelf
$ cd $(go env GOPATH)/bin && find . -type f | sort
./linux_arm64/shelf
./shelf
./windows_amd64/shelf.exe
With a version suffix, go install installs a command from a module, ignoring any go.mod in the current directory,
so installing a tool never changes the dependencies of the project you are standing in. This is the form that replaced
go get for installing programs (go get stopped building and installing in Go 1.18). The arguments must be main
packages, all with the same version query, all from one module. @latest and any other module query such as @v1.4.0,
@main or a commit hash work. To show it offline, the following was run against a throwaway module proxy holding
example.com/shelf at v1.4.0 and v1.5.0 (set up with GOPROXY=file://...); against the public proxy the commands are
identical:
$ go install example.com/shelf/cmd/shelf@v1.4.0
go: downloading example.com/shelf v1.4.0
$ go version -m $(go env GOBIN)/shelf | grep -E 'path|mod'
path example.com/shelf/cmd/shelf
mod example.com/shelf v1.4.0 h1:/B9oYXlO9SIvm2k2Fvuex1QfCupUUnmnD6abGcxB+xM=
$ go install example.com/shelf/cmd/shelf@latest
go: downloading example.com/shelf v1.5.0
$ go run example.com/shelf/cmd/shelf@v1.4.0 | head -2
shelf dev
os/arch: darwin arm64
go run pkg@version works the same way, which is how one-off tools are run without installing them. Use the tool
directive in go.mod (Go 1.24) for tools a project depends on, covered in
Modules and Dependency Management.
go clean
go clean removes what builds leave behind. Run without arguments inside a main package directory it deletes the
executable go build wrote there; the flags widen the reach:
| Flag | Removes |
|---|---|
|
the installed executable or archive as well (for example |
|
the same, recursively for all dependencies |
|
the entire build cache |
|
cached test results only |
|
cached fuzzing inputs that expanded coverage |
|
the downloaded module cache (everything under |
|
print the commands ( |
$ cd cmd/shelf && go build && ls
main.go shelf
$ go clean -x
rm -f /work/shelf/cmd/shelf/shelf
$ ls
main.go
$ go clean -i -n ./cmd/shelf
rm -f /work/bin/shelf
go clean -modcache is the right way to empty the module cache; deleting it by hand fails because the module cache
is read-only. Source: cmd/go: Remove object files and cached files
Build flags
The build flags are shared by build, clean, get, install, list, run and test. The full list is in
go help build; the ones used in practice:
| Flag | Effect |
|---|---|
|
output name or directory |
|
change directory first (Go 1.20); must be the first flag on the command line (Go 1.21) |
|
print packages, print commands, print without running, keep the work directory |
|
rebuild everything, ignoring the cache |
|
build in parallel with up to |
|
data race detector, memory sanitizer, address sanitizer (platform-limited; |
|
build a binary that records coverage into |
|
extra build tags, comma separated (the space-separated form is deprecated) |
|
replace file system paths in the binary by module paths |
|
arguments for the linker, compiler and assembler, optionally per package pattern |
|
stamp version-control data into the binary |
|
executable, archive, shared library, plugin; see below |
|
how |
|
build as if some files had different contents (editors and build systems use it) |
|
profile-guided optimization; |
|
run every toolchain program through a wrapper such as a static analyzer |
|
structured build output (Go 1.24) |
|
alternative compilers and package locations; rarely needed |
Three of them take a package pattern in front of the arguments, written pattern=args. Without a pattern an argument
list applies only to the packages named on the command line, not to their dependencies, which often surprises people
who add -gcflags=-m and see nothing for the libraries they use. all= applies to every package in the build, and a
... pattern selects a part of the module:
$ go build -gcflags='example.com/shelf/internal/...=-m' ./...
# example.com/shelf/internal/platform
internal/platform/lock_darwin.go:4:6: can inline LockDir
The latest match on the command line wins when patterns overlap; arguments with spaces are quoted inside the list,
as in -ldflags="-X 'main.commit=two words'".
-json
go build -json (and go install -json, go test -json) emits one JSON object per line instead of text, for tools
and CI systems. Each event has an ImportPath, an Action (build-output or build-fail) and, for output, the
text. Concatenating the Output fields gives the usual text. Here is a deliberately broken program:
package main
func main() {
var n int = "x"
}
$ go build -json ./...
{"ImportPath":"example.com/broken","Action":"build-output","Output":"# example.com/broken\n"}
{"ImportPath":"example.com/broken","Action":"build-output","Output":"./main.go:4:6: declared and not used: n\n"}
{"ImportPath":"example.com/broken","Action":"build-output","Output":"./main.go:4:14: cannot use \"x\" (untyped string constant) as int value in variable declaration\n"}
{"ImportPath":"example.com/broken","Action":"build-fail"}
Source: cmd/go: Build -json encoding and Go 1.24 release notes
Stamping a version into a binary
A binary should be able to say which version of the code it was built from. Go gives two mechanisms, and they are
complementary: the linker can set a string variable (-ldflags -X), and the go command records build information
that the program (or anyone with go version -m) can read.
-ldflags -X
-X importpath.name=value sets the value of a string variable at link time. For package main the import path is
literally main, even though the package has a longer path in go list. Other packages use their full import path.
// Package buildinfo holds values stamped in at link time.
package buildinfo
import "strings"
// Version is a plain string variable and can be set with -X.
var Version = "dev"
// Channel has an initializer that is a function call, which -X cannot override.
var Channel = strings.ToLower("STABLE")
// Limit is not a string, so -X is rejected.
var Limit = 10
Source: cmd/link
package main
import (
"fmt"
"runtime/debug"
"example.com/xdemo/buildinfo"
)
var commit = "unknown"
func main() {
fmt.Println("version:", buildinfo.Version)
fmt.Println("channel:", buildinfo.Channel)
fmt.Println("limit:", buildinfo.Limit)
fmt.Println("commit:", commit)
if bi, ok := debug.ReadBuildInfo(); ok {
fmt.Println("module:", bi.Main.Path, bi.Main.Version)
fmt.Println("go:", bi.GoVersion)
}
}
$ go run .
version: dev
channel: stable
limit: 10
commit: unknown
module: example.com/xdemo (devel)
go: go1.27.2
$ go run -ldflags "-X example.com/xdemo/buildinfo.Version=2.1.0 -X main.commit=abc123 -X example.com/xdemo/buildinfo.Channel=beta" .
version: 2.1.0
channel: stable
limit: 10
commit: abc123
module: example.com/xdemo (devel)
go: go1.27.2
$ go run -ldflags "-X example.com/xdemo/buildinfo.Limit=99" .
# example.com/xdemo
example.com/xdemo/buildinfo.Limit: cannot set with -X: not a var of type string (type:int)
The rules visible in that run: the variable must be a package-level string variable; it must be initialized with a
constant (or not at all) — Channel silently kept stable because its initializer is a function call that runs at
program start and overwrites the linker’s value; a non-string variable is a link error; a name that does not exist is
silently ignored, so a typo in the import path produces a binary that still says dev. A constant cannot be set at all
(it is not a variable). -X takes importpath.name=value, and the whole argument must be quoted when the value
contains spaces. Because a wrong path is silent, check the result in CI, for example by running the built binary with
--version.
$ go build -ldflags "-X main.nosuch=1" -o shelf ./cmd/shelf && ./shelf | head -1
shelf dev
Other linker flags used in release builds: -s omits the symbol table and -w omits DWARF debug information. Together
they shrink this program by about a third and make stack traces and debuggers less useful:
$ go build -o shelf ./cmd/shelf && go build -ldflags="-s -w" -o shelf-small ./cmd/shelf
$ wc -c < shelf; wc -c < shelf-small
2482674
1621330
Source: cmd/link
Build information inside every binary
Whatever flags are used, the go command records build information in the executable: the Go version, the main
module and its dependencies with versions and checksums, and the build settings that affect the result. Programs read it
with runtime/debug.ReadBuildInfo; people read it with go version -m, which works on any Go binary without source.
$ go version -m dist/shelf_1.4.0_linux_arm64
dist/shelf_1.4.0_linux_arm64: go1.27.2
path example.com/shelf/cmd/shelf
mod example.com/shelf (devel)
build -buildmode=exe
build -compiler=gc
build -trimpath=true
build CGO_ENABLED=0
build GOARCH=arm64
build GOOS=linux
build GOARM64=v8.0
Reading the lines: path is the main package, mod the main module (with (devel) when no version is known), and each
build line one setting. Settings appear when they were set or when they matter: -tags, -ldflags, -gcflags,
CGO_ENABLED and the C flags, GOOS, GOARCH, the microarchitecture variable of the target (GOARM64 here), and
vcs.*. Dependencies add dep lines (with => lines for replacements) when there are any. go version has more
forms:
$ go version
go version go1.27.2 darwin/arm64
$ go version vdir/ # walk a directory, report every Go binary found
vdir/a: go1.27.2
vdir/b: go1.27.2
$ go version -m -json shelf # runtime/debug.BuildInfo as JSON (Go 1.25)
Pass -v to report non-Go files found while walking. Because everything in go version -m is data inside the binary,
it is also what vulnerability scanners such as govulncheck read to decide which dependency versions a deployed file
contains.
-buildvcs and the main module’s version
By default (-buildvcs=auto) the go command adds the version-control revision, commit time and a modified flag to the
build information when the main package, its module and the current directory are in one repository. Go 1.18 added the
vcs.* settings; Go 1.24 additionally uses the same information to set the main module’s version, which used to be
(devel) for every local build:
-
at a commit that has a semantic-version tag, the version is that tag (
v1.6.0); -
with uncommitted changes,
+dirtyis appended; -
after the tag, a pseudo-version such as
v1.6.1-0.20261018000124-0f4bb3ed43a5; -
with no suitable tag,
v0.0.0-<time>-<commit>.
Here is the same module built four ways, in a Git repository whose first commit carries the tag v1.6.0. The output is
filtered from go version -m:
$ go build -o shelf ./cmd/shelf && go version -m shelf | grep -E '^\s+(mod|build\s+vcs)'
mod example.com/shelf v1.6.0
build vcs=git
build vcs.revision=dbd26c3a52a5a9a057441dc99b01b63958f1a7fb
build vcs.time=2026-10-18T00:12:01Z
build vcs.modified=false
$ echo "// c" >> internal/platform/platform.go && go build -o shelf ./cmd/shelf && go version -m shelf | grep -E '^\s+(mod|build\s+vcs.modified)'
mod example.com/shelf v1.6.0+dirty
build vcs.modified=true
$ git commit -qam "after tag" && go build -o shelf ./cmd/shelf && go version -m shelf | grep -E '^\s+mod'
mod example.com/shelf v1.6.1-0.20261018001201-331d9f4d374b
$ go build -buildvcs=false -o shelf ./cmd/shelf && go version -m shelf | grep -E '^\s+(mod|build\s+vcs)'
mod example.com/shelf (devel)
go run never stamps version-control data, and neither does a directory that is not under version control. -buildvcs=true
turns the quiet skipping into an error when the data exists but cannot be included (for example because the git program
is missing from a build container), which is a good setting for CI. -buildvcs=false is needed for reproducible
builds across different checkouts. A frequent surprise is a build inside a Docker image: when .git is not copied
into the build context the binary says (devel), and when it is copied but git is not installed -buildvcs=auto
silently omits the data. The simplest robust recipe is to compute the version in the pipeline and pass it with -X,
and to also leave -buildvcs on so both are recorded.
-trimpath and reproducible builds
A normal binary contains the absolute path of every source file (for stack traces and debuggers). That makes two builds
of identical source differ if the checkouts are in different directories. -trimpath rewrites those paths to module
paths (example.com/shelf/cmd/shelf/main.go), and records -trimpath=true in the build information. Combined with
-buildvcs=false (or identical repository state), the same source, toolchain and flags then give the same bytes
regardless of where the build runs:
$ go build -trimpath -buildvcs=false -o r1 ./cmd/shelf
$ (cp -R . ../copy && cd ../copy && go build -trimpath -buildvcs=false -o r2 ./cmd/shelf)
$ shasum -a 256 r1 r2 | cut -c1-16,65-
caadef944006e364 r1
caadef944006e364 r2
$ go build -buildvcs=false -o n1 ./cmd/shelf
$ (cd ../copy && go build -buildvcs=false -o n2 ./cmd/shelf)
$ shasum -a 256 n1 n2 | cut -c1-16,65-
bd00aeee13ac63af n1
c4afba0fd7afe3ed n2
(The second pair differs because the working directory is embedded.) One more consequence of -trimpath: the go command
then omits the -ldflags setting from the recorded build information, because linker flags can themselves contain
absolute paths (-extld, -extar, -extldflags); a binary built with -trimpath -ldflags "-s -w" shows only
-trimpath=true. Reproducibility also needs a fixed toolchain: the go line plus GOTOOLCHAIN selects it (see
Toolchain Versions and Compatibility), and
the Go project itself publishes toolchains that anyone can rebuild bit for bit.
go list
go list prints information about packages (or, with -m, modules). It is the scripting interface to the go command:
whatever the build would use — the file lists after build constraints, imports, dependencies, the module of each
package — is here as data, either as a Go template (-f) or as JSON (-json).
$ go list -f '{{.ImportPath}} -> {{join .Imports ", "}}' ./...
example.com/shelf/cmd/shelf -> example.com/shelf/internal/platform, fmt, runtime, runtime/debug
example.com/shelf/internal/platform ->
$ go list -f '{{.GoFiles}} ignored={{.IgnoredGoFiles}}' ./internal/platform
[debug_off.go lock_darwin.go platform.go] ignored=[debug_on.go lock_linux.go lock_other.go lock_windows.go]
$ go list -json=ImportPath,GoFiles ./internal/platform
{
"ImportPath": "example.com/shelf/internal/platform",
"GoFiles": [
"debug_off.go",
"lock_darwin.go",
"platform.go"
]
}
$ go list -deps -f '{{if not .Standard}}{{.ImportPath}}{{end}}' ./cmd/shelf
example.com/shelf/internal/platform
example.com/shelf/cmd/shelf
$ go list -e -f '{{.ImportPath}}: {{if .Error}}{{.Error.Err}}{{else}}ok{{end}}' ./nope ./cmd/shelf
./nope: stat /work/shelf/nope: directory not found
example.com/shelf/cmd/shelf: ok
$ go list -m
example.com/shelf
$ go list -m -f '{{.Path}} {{.GoVersion}}'
example.com/shelf 1.27
The fields most useful for builds are GoFiles, CgoFiles, IgnoredGoFiles, TestGoFiles, XTestGoFiles,
EmbedFiles, Imports, Deps, Standard, Module, Dir, Target and Stale. Flags change what is listed:
| Flag | Effect |
|---|---|
|
format with a |
|
JSON, optionally limited to the named fields |
|
also list every dependency, in dependency order |
|
also list the test variants of each package |
|
do not stop at packages with errors; report them in the |
|
fill |
|
build the packages and report the file with their compiled export data |
|
identify packages but do not resolve imports |
|
list modules, show available upgrades, list all versions, include retracted ones |
Because go list applies the same constraints as go build, it answers "which files does this target use" without
building: set GOOS, GOARCH or -tags in front of it. The sections on build constraints below use exactly that.
Source: cmd/go: List packages or modules
The environment
The go command is configured through environment variables, and go env prints the effective values. Values written
with go env -w live in a per-user file (found through os.UserConfigDir, overridable with the GOENV environment
variable) and are overridden by the real environment. go env -u removes a stored value, go env -changed prints only
settings that differ from the defaults (Go 1.23) and go env -json prints JSON. The sibling page on
toolchains shows the same commands for
GOTOOLCHAIN.
$ go env GOOS GOARCH CGO_ENABLED
darwin
arm64
1
$ go env -json GOOS GOARCH CGO_ENABLED
{
"CGO_ENABLED": "1",
"GOARCH": "arm64",
"GOOS": "darwin"
}
$ go env -w GOFLAGS=-trimpath
$ go env -changed | grep GOFLAGS
GOFLAGS='-trimpath'
$ go env -u GOFLAGS
The variables that matter for building (go help environment has the full list):
| Variable | Meaning |
|---|---|
|
target operating system and architecture; default to the host ( |
|
|
|
space-separated flags added to every go command that knows them; flags on the command line override it |
|
where |
|
default |
|
where downloaded modules live; default |
|
build cache directory (must be absolute); default |
|
a program that implements the build cache for the go command (Go 1.24) |
|
location of the file |
|
which toolchain to use (see the toolchains page) |
|
module download and authentication |
|
workspace file, or |
|
directory for temporary build files |
|
toolchain experiments, runtime/library behavior switches, FIPS 140-3 mode |
|
where binaries built with |
|
C toolchain settings used by cgo |
|
architecture level or features to compile for |
GOFLAGS is the way to give every build in a pipeline the same options without editing each command. Entries are
-flag=value pairs with no spaces in values, and flags unknown to a given subcommand are ignored, so one setting can serve
both build and test:
$ GOFLAGS="-tags=shelfdebug -trimpath" go build -o gf ./cmd/shelf
$ ./gf | sed -n 4,6p
debug: true
-tags = shelfdebug
-trimpath = true
$ go version -m gf | grep -E 'tags|trimpath'
build -tags=shelfdebug
build -trimpath=true
GOPATH deserves a sentence because older material makes it the center of everything: since modules became the default
in Go 1.16 there is no need to put code under $GOPATH/src, and GO111MODULE is only an escape hatch. See
Where the books are outdated.
The build cache and the test cache
Every package compilation and link is stored in the build cache under a key that hashes the source, the compiler and its flags, the build tags, the environment that matters and the dependencies. A build that finds the key reuses the result, so the second build above compiled nothing, and the edit-compile-run loop stays fast in large projects. Test results are cached the same way: a passing test is not re-run if nothing it depends on changed, and the output says so.
$ go test ./tf
ok example.com/bc/tf 0.174s
$ go test ./tf
ok example.com/bc/tf (cached)
$ go test -count=1 ./tf # -count=1 bypasses the test cache
ok example.com/bc/tf 0.056s
$ go clean -testcache && go test ./tf
ok example.com/bc/tf 0.061s
The cache is safe for concurrent go commands and is trimmed automatically of entries unused for some days. It tracks Go
files, the toolchain and flags but not the contents of C libraries linked through cgo, so after upgrading a C
library use -a or go clean -cache. Debugging it uses GODEBUG settings: gocachehash=1 prints the inputs of every
hash, gocachetest=1 explains test-cache decisions and gocacheverify=1 rebuilds everything and checks the results against the cache.
For CI, caching GOCACHE and GOMODCACHE between runs is the biggest single speed-up; Go 1.24 made GOCACHEPROG available
so that a build can share its cache through a remote service instead of a local directory.
Source: cmd/go: Build and test caching
go bug and go tool
go bug opens the default browser with a new issue on the Go issue tracker, prefilled with useful system information; it
is the intended way to report a toolchain problem. go tool runs a program from the
toolchain directory. After the Go 1.25 change that stopped shipping every tool pre-built, go tool with no argument lists
only the core ones that are installed, and others (such as pprof or trace) are built the first time they are used:
$ go tool
asm
cgo
compile
cover
fix
link
preprofile
vet
$ go tool dist list | wc -l
47
$ go tool covdata percent -i=covdata
example.com/shelf/cmd/shelf coverage: 100.0% of statements
example.com/shelf/internal/platform coverage: 100.0% of statements
The last command reads data written by a binary built with go build -cover and run with GOCOVERDIR=covdata: coverage
is not limited to go test; see Testing and
Profiling, Tracing and Diagnostics for
go tool pprof and go tool trace.
Build constraints
A build constraint (a build tag) says under which conditions a file takes part in the build. The go command evaluates constraints per file before parsing it, which is how one package can contain a Linux file, a Windows file and a portable fallback, and how tests, tools and experiments are kept out of normal builds. A file is included when it satisfies both its constraint line and its file name; the next figure shows the order the go command follows for each file of a package.
The tags that are true in a build are: the target GOOS and GOARCH; unix when the target is a Unix-like system (Go 1.19);
linux when the target is android and darwin when it is ios; cgo when cgo is enabled; the compiler name (gc
or gccgo); the architecture level tags such as amd64.v3; every release tag go1.1 up to the toolchain’s own
version; and anything given with -tags (including experiment tags such as goexperiment.jsonv2). Nothing else is true:
an unknown tag is simply false, which is what makes //go:build !shelfdebug the normal case.
The //go:build line
The line is //go:build followed by an expression using tags, !, &&, || and parentheses, with the usual
precedence (&& binds tighter than ||). It must appear before the package clause, preceded only by blank lines and
other line comments, and followed by a blank line. These files are the watch package of a second example module:
//go:build darwin || freebsd || netbsd || openbsd
package watch
// Backend names the notification API used on the BSD family.
const Backend = "kqueue"
Source: cmd/go: Build constraints
//go:build !linux && !darwin && !freebsd && !netbsd && !openbsd && !windows
package watch
// Backend names the fallback used everywhere else.
const Backend = "polling"
Source: cmd/go: Build constraints
//go:build amd64 && !purego
package watch
// Fast names the accelerated checksum implementation.
const Fast = "amd64"
The package also has watch_linux.go and watch_windows.go with no constraint line (their names are enough),
unix.go guarded by //go:build unix and notunix.go by //go:build !unix, fast_generic.go with the
negation of the constraint of fast_amd64.go, and a gen.go guarded by //go:build ignore that is a stand-alone generator.
Which files make it into the package for each target is visible with go list, without compiling anything:
$ for t in linux/amd64 darwin/arm64 windows/amd64 plan9/386 freebsd/amd64; do
> echo "$t: $(GOOS=${t%/*} GOARCH=${t#*/} go list -f '{{.GoFiles}}' ./watch)"
> done
linux/amd64: [fast_amd64.go unix.go watch.go watch_linux.go]
darwin/arm64: [fast_generic.go unix.go watch.go watch_bsd.go]
windows/amd64: [fast_amd64.go notunix.go watch.go watch_windows.go]
plan9/386: [fast_generic.go notunix.go watch.go watch_poll.go]
freebsd/amd64: [fast_amd64.go unix.go watch.go watch_bsd.go]
$ go list -tags purego -f '{{.GoFiles}}' ./watch # a custom tag, on the host (darwin/arm64)
[fast_generic.go unix.go watch.go watch_bsd.go]
$ GOOS=linux GOARCH=arm64 go list -f 'ignored: {{.IgnoredGoFiles}}' ./watch
ignored: [fast_amd64.go gen.go notunix.go watch_bsd.go watch_poll.go watch_windows.go]
go run gen.go still works for the ignore file because a file named on the command line is built whatever its constraint says (go run cmdfile/m.go runs a //go:build linux file on macOS, while go run ./cmdfile reports that the constraints exclude every file).
The convention is ignore for files meant to be run by hand and a project tag (purego, integration, shelfdebug)
for optional behavior. purego is the community name for "use no assembly"; many libraries switch their optimized
files off with it.
Source: cmd/go: Build constraints and go/build/constraint
File-name suffixes
A file whose name, before .go, ends in _GOOS, _GOARCH or _GOOS_GOARCH is constrained to that target, with no
constraint line needed: lock_linux.go, fast_arm64.go, syscall_windows_amd64.go. A file named exactly like a
known value (linux.go) is not constrained — the underscore is required — and the suffix is only recognized for
operating systems and architectures that Go knows. The trailing _test is checked first, so x_linux_test.go is a
Linux-only test file. It also works in the other direction, which surprises people: x_test_linux.go is a normal
Linux file, not a test file, because the name ends in _linux:
$ go list -f '{{.GoFiles}} {{.TestGoFiles}}' ./tf # on darwin: tf.go, tf_test.go, tf_test_linux.go
[tf.go] [tf_test.go]
$ GOOS=linux go list -f '{{.GoFiles}} {{.TestGoFiles}}' ./tf
[tf.go tf_test_linux.go] [tf_test.go]
Choose names with care: dns_android.go is a different thing from a file about Android devices if your package has no
Android story, and a new operating system or architecture added to Go can retroactively constrain a file whose name
happens to end with it. Prefer a constraint line for anything that is not clearly a platform file.
Release tags
Every go1.N up to the compiler’s version is true, so //go:build go1.27 includes a file only when built with Go 1.27 or
later, and //go:build !go1.27 provides the fallback. Two notes. The tag follows the toolchain, not the go line of
go.mod; the go line decides which language features are allowed. A library that must also build with older compilers
can use release tags to offer a fallback implementation, as in the example that follows.
$ go list -f '{{.GoFiles}}' ./ver # old.go: !go1.99, new.go: go1.99
[old.go]
$ go list -tags go1.99 -f '{{.GoFiles}}' ./ver
[new.go]
(A tag given with -tags is simply set; real release tags are only up to the compiler’s version, so go1.99 is false
unless forced.)
Architecture levels as tags
For the architectures with microarchitecture levels, the level is a tag too: amd64.v1..amd64.v4 (Go 1.18), the
matching forms arm64.v8.1 to arm64.v9.5 and riscv64.rva22u64, and so on. A higher level also satisfies the
lower tags, so with GOAMD64=v3 the tags amd64.v1, amd64.v2 and amd64.v3 all hold:
$ for v in v1 v2 v3 v4; do echo "GOAMD64=$v: $(GOOS=linux GOARCH=amd64 GOAMD64=$v go list -f '{{.GoFiles}}' ./micro)"; done
GOAMD64=v1: [base.go]
GOAMD64=v2: [base.go]
GOAMD64=v3: [v3.go]
GOAMD64=v4: [v3.go]
Here base.go is constrained by !amd64.v3 and v3.go by amd64.v3.
Mistakes that vet and the go command report
$ cat late/late.go
package late
//go:build linux
// Late is declared after the package clause, so the constraint above is ignored.
const Late = true
$ go vet ./late
late/late.go:3:1: misplaced //go:build comment
$ GOOS=windows go list -f '{{.GoFiles}}' ./late # the file is still included everywhere
[late.go]
A constraint after the package clause is silently ignored by the build, which is why the buildtag analyzer of go vet
reports it. The same analyzer checks the legacy form. Before Go 1.17 the syntax was // +build linux darwin (space is OR,
comma is AND, separate lines are AND). Go 1.17 introduced //go:build; gofmt adds the equivalent new line above an old one and keeps the two in sync. A
file that has only the old line is still honoured by the build, so old code keeps working; once both lines exist, the old
one can be deleted in a module with go 1.27. If both are present and disagree, go vet complains:
$ cat mis/m.go
//go:build linux
// +build darwin
package mis
$ GOOS=linux go vet ./mis
mis/m.go:2:1: +build lines do not match //go:build condition
$ gofmt -d legacy/legacy.go # a file with only "// +build linux"
--- legacy/legacy.go.orig
+++ legacy/legacy.go
@@ -1,3 +1,4 @@
+//go:build linux
// +build linux
package legacy
When every file of a package is excluded for the target, building it is an error naming the package; wildcard patterns quietly skip such packages:
$ cat onlylinux/only.go
//go:build linux
package onlylinux
$ GOOS=windows go build ./onlylinux
package example.com/bc/onlylinux: build constraints exclude all Go files in /work/bc/onlylinux
$ GOOS=windows go build ./... # no error: the pattern skips the package
$ GOOS=linux GOARCH=sparc go build ./watch
go: unsupported GOOS/GOARCH pair linux/sparc
Always test the other targets: GOOS=windows go vet ./... and GOOS=linux go vet ./... compile and analyze the files
your own platform excludes, which is the cheapest way to catch a missing file for a new platform.
Build modes
-buildmode chooses what kind of output the linker produces. The default builds executables for main packages and
archives (discarded) for the rest. On the host used here:
| Mode | Result | Notes |
|---|---|---|
|
a statically linked executable |
the normal case; |
|
position-independent executable |
security hardening; needed by some platforms |
|
|
|
|
a C static library plus a header |
only |
|
a C shared library ( |
same exporting rule; on |
|
Go packages as a shared library for |
rarely used; not supported with |
|
a Go plugin loadable with the |
Linux, macOS and FreeBSD; the host and plugin must match exactly; fragile |
A c-shared build needs a C compiler and cgo. This package exports one function and has an empty main, which a
shared library requires:
package main
import "C"
// ShelfDigits counts the digits in an ISBN-like string.
//
//export ShelfDigits
func ShelfDigits(s *C.char) C.int {
n := 0
for _, c := range C.GoString(s) {
if c >= '0' && c <= '9' {
n++
}
}
return C.int(n)
}
func main() {}
$ go build -buildmode=c-shared -o libshelf.dylib .
$ ls
csrc go.mod lib.go libshelf.dylib libshelf.h
$ grep ShelfDigits libshelf.h
extern int ShelfDigits(char* s);
$ clang csrc/main.c -I. -L. -lshelf -o callgo && DYLD_LIBRARY_PATH=. ./callgo
13
$ file libshelf.dylib
libshelf.dylib: Mach-O 64-bit dynamically linked shared library arm64
The C program csrc/main.c is #include "libshelf.h" plus a printf of ShelfDigits("978-0-13-419044-0"); it lives
in a subdirectory because a .c file next to Go files in the same package would be compiled by cgo. The details of
cgo, the C types and the rules for passing pointers are on the cgo and C Interoperability
page, and the WebAssembly mode is in WebAssembly and WASI.
Source: cmd/go: Build modes
Static or dynamic: cgo decides
A pure Go program on Linux is a static executable with no dependencies on the system: it makes system calls directly and
carries its own runtime, which is why a copy of it runs in an empty container image. The thing that changes this is
cgo. A few standard-library packages use C on some systems — net (the system resolver), os/user and
runtime/cgo on Linux — and then the executable is linked dynamically against the C library. go list shows the
cgo files per target without building:
$ GOOS=linux CGO_ENABLED=1 go list -f '{{.ImportPath}}: cgo={{.CgoFiles}}' net os/user
net: cgo=[cgo_linux.go cgo_resnew.go cgo_socknew.go cgo_unix_cgo.go cgo_unix_cgo_res.go]
os/user: cgo=[cgo_lookup_cgo.go getgrouplist_unix.go]
$ GOOS=linux CGO_ENABLED=1 go list -deps -f '{{if .CgoFiles}}{{.ImportPath}}{{end}}' net os/user
runtime/cgo
net
os/user
$ GOOS=linux CGO_ENABLED=1 go list -tags netgo,osusergo -deps -f '{{if .CgoFiles}}{{.ImportPath}}{{end}}' net os/user
$
The last command is the empty one: the tags netgo and osusergo select the pure-Go implementations of name
resolution and user lookup, so no cgo is involved. (CGO_ENABLED=0 has the same effect for these two and for every other
package that has a cgo-free fallback.) The practical rules:
-
a program with cgo dependencies built with
CGO_ENABLED=1links the platform C library dynamically; macOS always linkslibSystem(otool -Lshows/usr/lib/libSystem.B.dylibeven for the plain Go program built above) and Windows imports system DLLs; -
CGO_ENABLED=0forces a build without cgo; packages that need C fail to compile with a message naming the missing functionality, which is correct because there is no fallback; -
cross-compiling turns
CGO_ENABLEDoff by default because no C cross-compiler is configured:go env CGO_ENABLEDprints1natively but0underGOOS=linux GOARCH=arm64on this machine; -
a fully static binary that uses C code needs an external link against a static C library (
-ldflags "-linkmode external -extldflags -static"on Linux); that is a C-toolchain exercise, and the usual advice is to avoid cgo in programs that must be portable.
Cross-compilation
The Go toolchain is a cross compiler out of the box: the compiler, linker and standard library can target any supported
combination of operating system and architecture, with no separate toolchain to install, as long as cgo is not involved.
Set GOOS and GOARCH for the single command:
$ for t in linux/amd64 linux/arm64 windows/amd64 darwin/amd64 freebsd/amd64; do
> GOOS=${t%/*} GOARCH=${t#*/} go build -o out/shelf-${t%/*}-${t#*/} ./cmd/shelf
> done
$ file out/shelf-*
out/shelf-darwin-amd64: Mach-O 64-bit executable x86_64
out/shelf-freebsd-amd64: ELF 64-bit LSB executable, x86-64, version 1 (FreeBSD), statically linked
out/shelf-linux-amd64: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked
out/shelf-linux-arm64: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked
out/shelf-windows-amd64: PE32+ executable (console) x86-64, for MS Windows
(file also prints build identifiers, omitted here.) Every build used the files that match its target, so each binary has
the right lock_*.go compiled in. go tool dist list prints every supported pair, -json adds whether cgo works and
whether the port is first class (a port with continuous builders and the strongest support commitment), and -broken
adds the pairs that exist but are known broken:
$ go tool dist list | wc -l
47
$ go tool dist list -json | head -6
[
{
"GOOS": "aix",
"GOARCH": "ppc64",
"CgoSupported": true,
"FirstClass": false
},
$ go tool dist list -broken | wc -l
49
Of those 47 pairs, 41 support cgo and eight are first class on Go 1.27: darwin/amd64, darwin/arm64, linux/386,
linux/amd64, linux/arm, linux/arm64, windows/386 and windows/amd64. The others by operating system:
| GOOS | GOARCH values in Go 1.27 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
An unsupported pair is a clear error (go: unsupported GOOS/GOARCH pair linux/sparc). The list changes between releases;
ask go tool dist list rather than trusting a table, and see the
minimum requirements for the operating-system versions each release needs.
A release build
Putting the flags together gives the usual release recipe: no cgo, paths trimmed, symbols stripped, the version set by the linker, and the checksum file next to the binaries. The script was run for real:
#!/bin/sh
# Build the shelf command for several targets, the way a release script would.
set -eu
version=${1:?usage: release.sh VERSION}
export CGO_ENABLED=0
rm -rf dist && mkdir dist
for target in linux/amd64 linux/arm64 darwin/arm64 windows/amd64; do
os=${target%/*}
arch=${target#*/}
ext=""
[ "$os" = windows ] && ext=".exe"
GOOS=$os GOARCH=$arch go build -trimpath -buildvcs=false \
-ldflags "-s -w -X main.version=$version" \
-o "dist/shelf_${version}_${os}_${arch}${ext}" ./cmd/shelf
done
(cd dist && shasum -a 256 shelf_* > SHA256SUMS)
$ ./release.sh 1.4.0 && ls -l dist | awk 'NR>1{print $5, $9}'
367 SHA256SUMS
1621314 shelf_1.4.0_darwin_arm64
1560736 shelf_1.4.0_linux_amd64
1573024 shelf_1.4.0_linux_arm64
1703936 shelf_1.4.0_windows_amd64.exe
$ cat dist/SHA256SUMS | cut -c1-16,65-
cc9147dc8085793c shelf_1.4.0_darwin_arm64
cbc920748ed906ed shelf_1.4.0_linux_amd64
807c814e05377dad shelf_1.4.0_linux_arm64
d984a3a233191966 shelf_1.4.0_windows_amd64.exe
$ ./dist/shelf_1.4.0_darwin_arm64 | head -1
shelf 1.4.0
For tagged releases with archives, signing, a Homebrew tap and container images, do not hand-write this script: GoReleaser
drives exactly these go build calls from a configuration file, and the
Packaging and Distribution page of the command-line guide covers
it together with checksums and attestations. The Go side to remember is -trimpath plus a recorded version so that a
user can run go version -m on the file they downloaded.
Microarchitecture levels
Within one architecture the compiler can target newer instruction sets for speed, at the price of not running on older processors. The setting is an environment variable read at build time and recorded in the build information:
| Variable | Values (default first) |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$ GOOS=linux GOARCH=amd64 GOAMD64=v3 go build -o shelf-v3 ./cmd/shelf
$ go version -m shelf-v3 | grep GOAMD64
build GOAMD64=v3
Which level to pick is a deployment decision: v3 is a safe floor for servers bought in the last decade and meaningfully
faster for numeric code, but a binary built with it exits with an illegal-instruction error on an older CPU, so it
needs the same care as an operating-system minimum version.
Response files
Go 1.27 added response file support to the individual toolchain programs: compile, link, asm, cgo, cover and
pack now expand an argument @file into the arguments read from that file. The format is whitespace-separated
arguments with single-quoted and double-quoted strings, escape sequences and backslash-newline continuation, compatible with
GCC’s response files. Its purpose is the very long command lines that build systems such as Bazel generate, which can
exceed operating-system limits. It works when a program invokes the tools directly with go tool:
$ cat compile.rsp
-p example.com/hello
-lang=go1.27
-o hello.a
-pack
hello.go
$ go tool compile @compile.rsp && go tool pack t hello.a
__.PKGDEF
_go_.o
$ printf -- '-p "example.com/hello"\n-o hello2.a -pack \\\n hello.go\n' > c2.rsp
$ go tool compile @c2.rsp && ls hello2.a
hello2.a
$ go tool compile @nope.rsp
compile: open nope.rsp: no such file or directory
It is not an option of the go command itself, and it cannot be passed through -ldflags or -gcflags. The source
of the go command explains why: GNU tools treat an @foo argument anywhere on the command line as a response file, so the
go command refuses to run (or print) any tool command line that contains an argument beginning with @:
$ go build @args.rsp
package .@args.rsp: can only use path@version syntax with 'go get' and 'go install' in module-aware mode
$ go build -ldflags="-s @link.rsp" -o shelf ./cmd/shelf
example.com/shelf/cmd/shelf: invalid command-line argument @link.rsp in command: link -o ... -s @link.rsp ...
The linker itself does understand the file when run by hand with the same arguments the go command used (found with
go build -x -work):
$ cat link.rsp
-X
main.version=9.9.9-rsp
-s
-w
$ go tool link -o a1 -importcfg $WORK/b001/importcfg.link -buildmode=pie -extld=clang @link.rsp $WORK/b001/_pkg_.a
$ ./a1 | head -1
shelf 9.9.9-rsp
So for ordinary projects the answer remains -ldflags on the command line, GOFLAGS, or a Makefile variable; response
files matter to people writing build systems on top of the toolchain. The release notes for Go 1.27 are the primary
source for the feature.
Source: Go 1.27 release notes: Tools and cmd/compile
What changed, by release
| Release | Change to the go command or the build |
|---|---|
1.13 |
|
1.16 |
modules on by default; |
1.17 |
|
1.18 |
|
1.19 |
the |
1.20 |
|
1.21 |
|
1.23 |
|
1.24 |
main module version from VCS tags and commits; |
1.25 |
|
1.26 |
|
1.27 |
response files for the toolchain programs; |
Source: Go 1.24 release notes, Go 1.25 and Go 1.27
Where the books are outdated
Both books were written when the build story was a workspace and a handful of commands. Bodner’s chapter 1 installs
Go, runs go run, go build and go install, and shows go install of a program from a module path with a version
suffix, which was new in Go 1.16 and is still the right form; its Makefile advice stands. What it cannot show is
everything added after Go 1.15: the build information and go version -m, the main module’s version taken from the
repository (Go 1.24), -buildvcs, -trimpath and reproducible builds as a supported goal, go build -json, the
GOAMD64 family, and automatic toolchain selection. Where the book discusses cross-compiling and GOOS/GOARCH the
variables are right but the table of ports is older, and CGO_ENABLED=0 is not explained as the rule it now is for
portable containers.
Vivien’s chapter 6 builds and installs packages inside $GOPATH/src, with go get as the way to fetch and compile
remote code and go install placing results under $GOPATH/bin. In modules go get only changes go.mod, code
lives anywhere, and GOPATH is merely the home of bin/ and pkg/mod; vendoring behaves differently; and
//go:build replaced // +build in Go 1.17, so any file in that style should be run through gofmt. Its Go 1.6/1.7 era
GOOS/GOARCH list predates the first-class ports and the arm64, riscv64 and loong64 targets. Both books pre-date
go install pkg@version replacing go get -u for tools, -trimpath, build constraints for unix, build caching as a
tested guarantee, and the go env -w file; none of them mentions response files.
Common pitfalls
-
-ldflags -Xfails silently on a wrong import path (for packagemainthe path ismain, not the long import path), a misspelled name, or a variable with a function-call initializer; check the output once. -
Without
-trimpaththe binary embeds your build machine’s paths; with-trimpaththe recorded-ldflagsdisappear fromgo version -m. -
go build ./...with severalmainpackages builds nothing visible; use-o dir/. -
go runreturns exit status 1 for any failing program; run the built binary if the exact status matters. -
-gcflags=-mapplies only to the named packages unless writtenall=-m. -
A
//go:buildline afterpackageis ignored; a file namedx_test_linux.gois not a test file. -
A cross-compiled build is silently cgo-free; a package that needs cgo will fail to compile for the target with a message about missing C code.
-
The build cache ignores changes in C libraries; use
-aafter upgrading one. -
GOFLAGSapplies to every go command in the environment, including those of your colleagues' scripts; prefer explicit flags in shared Makefiles.
References
-
cmd/go — the go command reference: build, run, install, clean, list, version, env
-
cmd/link and cmd/compile