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)
			}
		}
	}
}

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

go build

compile packages and dependencies; write the executable for a single main package

this page

go run

compile a main package into a temporary directory and run it

this page

go install

build and put executables in GOBIN; with @version, install a command from a module

this page

go clean

remove build products and caches

this page

go env

print or change the environment

this page and Toolchain Versions and Compatibility

go list

print information about packages and modules, optionally through a template

this page

go version

print the toolchain version, or the build information inside a binary

this page

go bug

open a browser with a new bug report prefilled with system information

this page

go tool

run a toolchain program such as compile, link, cover or vet directly

this page

go get, go mod, go work

dependencies, go.mod and workspaces

Modules and Dependency Management, Workspaces, Versioning and Publishing

go test

build and run tests, benchmarks and fuzz targets

Testing

go vet, go fmt, go fix

static analysis and formatting

Formatting, vet, fix and Static Analysis

go generate

run //go:generate commands

Directives, Code Generation and Embedding

go doc

print documentation

Documentation and Doc Comments

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 main package 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/v2 yields foo, with .exe added on Windows); a list of .go files 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;

  • -o overrides 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

-i

the installed executable or archive as well (for example $GOBIN/shelf)

-r

the same, recursively for all dependencies

-cache

the entire build cache

-testcache

cached test results only

-fuzzcache

cached fuzzing inputs that expanded coverage

-modcache

the downloaded module cache (everything under GOMODCACHE, read-only files included)

-n, -x

print the commands (-n without running them), as for go build

$ 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

-o file-or-dir

output name or directory

-C dir

change directory first (Go 1.20); must be the first flag on the command line (Go 1.21)

-v, -x, -n, -work

print packages, print commands, print without running, keep the work directory

-a

rebuild everything, ignoring the cache

-p n

build in parallel with up to n programs (default GOMAXPROCS)

-race, -msan, -asan

data race detector, memory sanitizer, address sanitizer (platform-limited; -asan detects leaks of C memory by default since Go 1.25)

-cover, -covermode, -coverpkg

build a binary that records coverage into GOCOVERDIR (Go 1.20)

-tags a,b

extra build tags, comma separated (the space-separated form is deprecated)

-trimpath

replace file system paths in the binary by module paths

-ldflags, -gcflags, -asmflags

arguments for the linker, compiler and assembler, optionally per package pattern

-buildvcs=auto|true|false

stamp version-control data into the binary

-buildmode

executable, archive, shared library, plugin; see below

-mod=readonly|vendor|mod, -modfile, -modcacherw

how go.mod, go.sum and the module cache may change

-overlay file.json

build as if some files had different contents (editors and build systems use it)

-pgo file|auto|off

profile-guided optimization; auto picks default.pgo in the main package (default since Go 1.21)

-toolexec 'cmd args'

run every toolchain program through a wrapper such as a static analyzer

-json

structured build output (Go 1.24)

-compiler gc|gccgo, -installsuffix, -pkgdir, -linkshared

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"}

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, +dirty is 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

-f template

format with a text/template over the Package (or Module) struct; join is predefined

-json[=fields]

JSON, optionally limited to the named fields

-deps

also list every dependency, in dependency order

-test

also list the test variants of each package

-e

do not stop at packages with errors; report them in the Error field

-compiled

fill CompiledGoFiles with the files actually given to the compiler (cgo output included)

-export

build the packages and report the file with their compiled export data

-find

identify packages but do not resolve imports

-m, -u, -versions, -retracted

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.

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

GOOS, GOARCH

target operating system and architecture; default to the host (GOHOSTOS, GOHOSTARCH)

CGO_ENABLED

1 to allow cgo, 0 to disable it; default 1 only when building natively with a C compiler available

GOFLAGS

space-separated flags added to every go command that knows them; flags on the command line override it

GOBIN

where go install puts executables; default $GOPATH/bin

GOPATH

default $HOME/go; today it only holds bin/ and the module cache under pkg/mod

GOMODCACHE

where downloaded modules live; default $GOPATH/pkg/mod

GOCACHE

build cache directory (must be absolute); default ~/Library/Caches/go-build on macOS, ~/.cache/go-build on Linux, %LocalAppData%\go-build on Windows

GOCACHEPROG

a program that implements the build cache for the go command (Go 1.24)

GOENV

location of the file go env -w writes; off disables it

GOTOOLCHAIN

which toolchain to use (see the toolchains page)

GOPROXY, GOSUMDB, GOPRIVATE, GONOSUMDB, GONOPROXY, GOINSECURE, GOAUTH, GOVCS

module download and authentication

GOWORK

workspace file, or off

GOTMPDIR

directory for temporary build files

GOEXPERIMENT, GODEBUG, GOFIPS140

toolchain experiments, runtime/library behavior switches, FIPS 140-3 mode

GOCOVERDIR

where binaries built with -cover write their data

CC, CXX, CGO_CFLAGS, CGO_LDFLAGS, PKG_CONFIG

C toolchain settings used by cgo

GOAMD64, GOARM, GOARM64, GO386, GOMIPS, GOPPC64, GORISCV64, GOWASM

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.

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.

flowchart TD A["A .go file in the package directory"] --> B{"Name ends in _test.go?"} B -->|"yes"| T["Test file: only go test, vet and similar use it"] B -->|"no"| C{"Name ends in _GOOS, _GOARCH or _GOOS_GOARCH?"} C -->|"yes, and it does not match the target"| X["Excluded: listed in IgnoredGoFiles"] C -->|"no suffix, or the suffix matches"| D{"Has a //go:build line before the package clause?"} D -->|"no"| I["Included in the build"] D -->|"yes"| E{"Does the expression evaluate to true?"} E -->|"true"| I E -->|"false"| X I --> F{"Any file left for the package?"} X --> F F -->|"none"| G["Error: build constraints exclude all Go files"] F -->|"some"| H["Compile the 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"
//go:build !linux && !darwin && !freebsd && !netbsd && !openbsd && !windows

package watch

// Backend names the fallback used everywhere else.
const Backend = "polling"
//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.

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

default or exe

a statically linked executable

the normal case; pie is the default on some platforms

pie

position-independent executable

security hardening; needed by some platforms

archive

.a files for non-main packages

go build with multiple packages discards them; mostly for tooling

c-archive

a C static library plus a header

only //export-ed functions are callable; one main package

c-shared

a C shared library (.so, .dylib, .dll) plus a header

same exporting rule; on wasip1 builds a WASI reactor

shared

Go packages as a shared library for -linkshared

rarely used; not supported with -o

plugin

a Go plugin loadable with the plugin package

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.

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=1 links the platform C library dynamically; macOS always links libSystem (otool -L shows /usr/lib/libSystem.B.dylib even for the plain Go program built above) and Windows imports system DLLs;

  • CGO_ENABLED=0 forces 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_ENABLED off by default because no C cross-compiler is configured: go env CGO_ENABLED prints 1 natively but 0 under GOOS=linux GOARCH=arm64 on 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

linux

386, amd64, arm, arm64, loong64, mips, mips64, mips64le, mipsle, ppc64, ppc64le, riscv64, s390x

windows

386, amd64, arm64

darwin

amd64, arm64 (macOS 13 or later since Go 1.27)

freebsd, netbsd

386, amd64, arm, arm64

openbsd

386, amd64, arm, arm64, ppc64, riscv64

android

386, amd64, arm, arm64

ios

amd64, arm64

js, wasip1

wasm

aix, dragonfly, illumos, plan9, solaris

ppc64 (aix); amd64 (dragonfly, illumos, solaris); 386, amd64, arm (plan9)

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)

GOAMD64

v1, v2, v3, v4 (Go 1.18); v3 assumes AVX2, BMI and FMA (see the wiki page on minimum requirements)

GOARM64

v8.0 to v8.9, v9.0 to v9.5, optionally followed by ,lse or ,crypto (Go 1.23)

GOARM

5, 6, 7 with ,softfloat or ,hardfloat; the default depends on the host

GORISCV64

rva20u64, rva22u64, rva23u64 (Go 1.23)

GOPPC64

power8, power9, power10

GO386

sse2, softfloat

GOMIPS, GOMIPS64

hardfloat, softfloat

GOWASM

satconv, signext

$ 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.

What changed, by release

Release Change to the go command or the build

1.13

-trimpath and go version -m

1.16

modules on by default; go install pkg@version; -overlay

1.17

//go:build constraints

1.18

-buildvcs and vcs.* build settings; GOAMD64; go get no longer installs

1.19

the unix constraint

1.20

-C dir; -cover for binaries; -pgo

1.21

-C must come first; -pgo=auto is the default; toolchain management with GOTOOLCHAIN

1.23

go env -changed; GOARM64 and GORISCV64

1.24

main module version from VCS tags and commits; go build -json; GOCACHEPROG; tool directives

1.25

go version -m -json; go build -asan detects leaks; ignore directive; fewer pre-built tools; work pattern

1.26

go fix rewritten as the home of modernizers (see the formatting page); go mod init writes a lower go version

1.27

response files for the toolchain programs; bzr no longer supported for fetching modules; -macos and -macsdk linker options on macOS

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 -X fails silently on a wrong import path (for package main the path is main, not the long import path), a misspelled name, or a variable with a function-call initializer; check the output once.

  • Without -trimpath the binary embeds your build machine’s paths; with -trimpath the recorded -ldflags disappear from go version -m.

  • go build ./... with several main packages builds nothing visible; use -o dir/.

  • go run returns exit status 1 for any failing program; run the built binary if the exact status matters.

  • -gcflags=-m applies only to the named packages unless written all=-m.

  • A //go:build line after package is ignored; a file named x_test_linux.go is 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 -a after upgrading one.

  • GOFLAGS applies to every go command in the environment, including those of your colleagues' scripts; prefer explicit flags in shared Makefiles.