Your Teammate Builds in Half a Second, Your Machine Says Command Not Found
A teammate sends you a repository and a one-line instruction: go build. You clone it, type the command, and your shell replies that go is not recognized. Or worse: the build starts and dies with a GOROOT error while their machine compiles in half a second. The repository is identical on both machines. The difference is the toolchain, and three questions decide everything: which Go is installed, where does it live, and can your shell find it.
Installing Go is not dropping a single binary somewhere. The distribution is one self-contained directory holding the go command, the compiler, the assembler, the linker, the standard library source, and the companion tools (gofmt, vet, doc, pprof). Nothing else needs to match its version — there is no separate standard-library package to keep in sync, and pure Go programs do not need a C compiler. That wholeness is why a Go install is easy to verify: if go version answers, everything in that directory is present.
The installer puts that directory in a conventional place and, on Windows and macOS, adds its bin subdirectory to PATH for you. On Linux the tarball lands wherever you extract it (/usr/local/go by convention), and adding bin to PATH is your job.
Windows C:\Program Files\Go\bin (MSI installer, PATH updated automatically)
macOS /usr/local/go/bin (pkg installer, PATH updated automatically)
Linux /usr/local/go/bin (tarball; append to PATH yourself)
Then verify three things:
go version # go1.24.1 windows/amd64 — version AND target platform
which go # /usr/local/go/bin/go — which file the shell runs
go env GOROOT # /usr/local/go — the installation that binary belongs to
The subtle failure mode appears when a machine has two Go installations: a leftover tarball in /usr/local/go, a package-manager copy at /opt/homebrew/bin/go, an old version's bin on PATH ahead of both. PATH is searched in order, so the first go wins for your shell while your editor may resolve a different one from its own settings. The symptom is a go version that disagrees with the GOROOT your IDE reports, or a gofmt that behaves differently in CI than on your laptop.
One command collapses the whole class of problems: go env GOROOT must name the parent directory of the go that which go reported. If it does not, two installations are mixed, and you remove or re-order one until they agree. Everything in this phase — running programs, formatting, vetting — is executed by tools inside that one directory, so the invariant is worth checking before anything else. Once it holds, move to Your First Go Program and make the compiler run.
Reading the Machine's Configuration with go env
go version tells you what you have. go env tells you how it behaves. Running it with no arguments dumps every setting as shell syntax — useful in bug reports — but the real skill is asking for the handful of variables that change how builds work.
| Variable | The question it answers | Typical default |
|---|---|---|
GOROOT |
Where does this go binary's installation live? |
Inferred from the binary's own location |
GOPATH |
Where is my per-user workspace of caches and tools? | $HOME/go (Windows: %USERPROFILE%\go) |
GOBIN |
Where does go install put executables? |
Empty → $GOPATH/bin |
GOMODCACHE |
Where is downloaded dependency source stored? | $GOPATH/pkg/mod |
GOCACHE |
Where are compiled build artifacts cached? | OS cache directory |
GOPROXY |
Where are modules fetched from? | proxy.golang.org,direct |
GOTOOLCHAIN |
How does go.mod pick the Go version? | auto |
GOOS / GOARCH |
What platform is this build for? | The host's platform |
GOPATH deserves a warning because its meaning changed. Before modules, your source code lived in $GOPATH/src and imports resolved against that directory. That era is over: code now lives wherever you cloned it, dependencies live in the module cache, and $GOPATH survives only as the home of the module cache and installed binaries. Putting new code in $GOPATH/src buys nothing — go.mod and Semantic Versioning is where project layout is actually defined now.
Settings come in two flavors. Environment variables you export in a shell apply to that shell only, which makes them useless for the IDE or a CI job that never saw your profile. go env -w writes the value to a per-user configuration file that every go invocation reads:
go env -w GOPROXY=https://proxy.corp.example,direct # persist a corporate proxy
go env -u GOPROXY # revert to the built-in default
go env GOPROXY # print the effective value
This is the right place for machine-specific configuration — GOPROXY, GOTOOLCHAIN, GOFLAGS — because it is one file, visible with plain go env, and does not depend on which shell or editor spawned the process.
GOOS and GOARCH are not cosmetic. They select the target platform of the build, which is how one installation of Go produces binaries for every operating system Go supports:
GOOS=linux GOARCH=amd64 go build -o bin/server ./cmd/server
# PowerShell: $env:GOOS = 'linux'; $env:GOARCH = 'amd64'; go build -o bin/server ./cmd/server
Cross-compiling works because the Go toolchain compiles Go itself rather than shelling out to a host C toolchain, and when GOOS/GOARCH differ from the machine you are on, cgo is disabled automatically. The output is a self-contained binary you can scp to a server of a different platform without installing anything else. That single capability is one of the reasons Go spread to infrastructure work: build once on a laptop, deploy anywhere Go runs.
One Install, Every Version: How go.mod Chooses Your Toolchain
A repository you join may require Go 1.24 while your laptop runs 1.22. Older Go told you to uninstall and reinstall. Modern Go makes the version a property of the project instead of the machine.
Inside go.mod, two directives control it:
module example.com/orders
go 1.23.0
toolchain go1.24.1
The go line is a floor: this module may use language features introduced up to that version, and any Go at or above it can build the module. The toolchain line is a preference: the toolchain the module's authors developed with. With GOTOOLCHAIN=auto — the default — if the go binary on your machine is older than what go.mod requires, the go command fetches the right toolchain from the module proxy, stores it in the module cache, and runs it. Your teammate with Go 1.19 checks out the repo, runs go build, and it works; no manual upgrade, no corrupted installation.
The counter-setting matters on locked-down machines: GOTOOLCHAIN=local refuses every download and fails loudly if go.mod demands newer, so CI can enforce an exact image. Pinning one version for a single command is GOTOOLCHAIN=go1.23.4 go test ./.... All three forms persist with go env -w when they should be the default.
go env -w GOTOOLCHAIN=local # never download; fail instead
go env GOTOOLCHAIN # auto | local | <version> | path...
The go line has a consequence beyond downloading: language semantics can change with it. Module code declaring go 1.21 keeps pre-1.22 loop-variable behavior even when built by a newer compiler, while go 1.22 or later gets per-iteration loop variables. You will meet that exact trap in Loops with for; the point here is that the toolchain version and the go line together define what your code means, not just which compiler runs.
For deliberate side-by-side testing — reproducing a bug that only appears on an old release — the legacy helpers still work: go install golang.org/dl/go1.21.0@latest followed by go1.21.0 download installs that SDK under ~/sdk as a separate command. In daily work, GOTOOLCHAIN covers nearly everything, and the SDK trick is for the rare bisect across releases.
One last PATH loop closes this topic. Installing a developer tool drops its executable in $GOBIN, defaulting to $GOPATH/bin:
go install golang.org/x/tools/gopls@latest
gopls version # 'gopls: command not found' means $GOPATH/bin is not on PATH
The failure is the same PATH problem from the first chapter in a new costume: go found its way onto PATH during install, but the directory where go install writes binaries did not. Append $GOPATH/bin once and both the toolchain and its tools resolve from known locations.
A working setup, verified in three lines:
go version && go env GOROOT GOTOOLCHAIN GOPROXY
# go1.24.1 windows/amd64
# C:\Program Files\Go
# auto
# proxy.golang.org,direct
With that output matching expectations, the environment is done. Your First Go Program can assume every command in it exists and runs the version you think it does.
Quiz
1. Your shell runs `go version` and reports go1.24.1, but your editor's language server insists GOROOT is /usr/local/go1.19 and refuses to build the project. What is the most likely cause?
2. GOBIN is empty and you run `go install golang.org/x/tools/gopls@latest` with GOPATH at its default. Where does the gopls executable land?
3. A repo's go.mod contains `go 1.24.0`, your machine runs go1.22.3, and GOTOOLCHAIN is at its default. What happens when you run `go build`?
4. You must produce a linux/amd64 binary from your Windows development machine with one standard Go installation. Which approach is correct?
Flashcards
Question
What does `go env GOROOT` report, and what is the red flag when you run it?
Click to reveal answer
Answer
GOROOT is the root directory of the installation that the `go` binary you just invoked belongs to — the parent of its bin directory. The red flag is a value that does not match the location `which go` (or `where go`) reported, because that means two installations are on the machine and different tools are being resolved by your shell, your editor, and CI.
Question
What is GOPATH used for today, and what was it used for before Go modules?
Click to reveal answer
Answer
Today GOPATH is only a per-user workspace: it holds the module cache (GOMODCACHE, under pkg/mod) and executables written by `go install` (bin/). Before modules it was where your source lived, under $GOPATH/src, and imports resolved against that directory — a layout that no longer applies to new projects.
Question
Where do `go install` binaries go when GOBIN is unset, and what usually goes wrong next?
Click to reveal answer
Answer
They go to $GOPATH/bin, defaulting to $HOME/go/bin when GOPATH itself is unset. The follow-on failure is PATH: the toolchain's bin directory is on PATH from installation, but $GOPATH/bin usually is not, so the freshly installed command (gopls, staticcheck) is reported as not found until you append that directory.
Question
How does GOTOOLCHAIN=auto behave when go.mod requires a Go version newer than the installed one, and how do you turn that off?
Click to reveal answer
Answer
The go command downloads the required toolchain from the module proxy into the module cache and runs it transparently — no manual reinstall. Setting GOTOOLCHAIN=local (via `go env -w`) disables all downloads so the build fails loudly instead, which is how CI images pin an exact toolchain.
Question
Which settings change the target platform of a build, and what happens to cgo when they differ from the host?
Click to reveal answer
Answer
GOOS selects the operating system and GOARCH the architecture; setting both for one command cross-compiles (for example GOOS=linux GOARCH=amd64 go build). When the target differs from the host machine, cgo is disabled automatically, so pure-Go programs still build and the result is a self-contained binary for the target platform.
Revision Notes
Key Takeaways
- 1. One directory holds the compiler, standard library, and tools; if `go version` answers, the install is whole — PATH order decides which install you actually run
- 2. `go env GOROOT` must match the `go` on your PATH; a mismatch means two installations and explains editor-versus-terminal disagreements
- 3. GOPATH no longer stores your code — it holds the module cache and `go install` output; project layout comes from go.mod, not GOPATH
- 4. go.mod's `go` line is a floor and GOTOOLCHAIN=auto fetches anything newer, so toolchain version becomes a property of the repository rather than the machine
- 5. GOOS/GOARCH turn one installation into a cross-compiler; cgo drops out automatically when the target differs from the host
Interview Tips
- • "How do you debug 'works on my machine' for a build?" — start with `go version`, `which go`, and `go env GOROOT` on both machines and compare; the answer shows you localize the layer (PATH, install, config) instead of guessing
- • "What does GOPATH do in a modules world?" — name its two survivors (module cache, installed binaries) and state clearly that source layout comes from go.mod; mentioning the old $GOPATH/src era shows historical depth
- • "How would you pin the Go version for CI?" — GOTOOLCHAIN=local plus the `go` line in go.mod, so a missing toolchain fails the build rather than silently downloading one
- • "Explain cross-compilation in Go" — GOOS/GOARCH are read by the go command, no second toolchain, cgo disabled automatically; add that the result is a static binary for the target
Cheat Sheet
Installing the Go Toolchain
Verify the installation (in this order):
| Check | Command | Passes when |
|---|---|---|
| Version and target | go version |
prints go1.x.y <os>/<arch> you expect |
| Which binary runs | which go / where go |
points at one known install |
| Consistent root | go env GOROOT |
equals the parent of that binary's bin/ |
| Effective config | go env GOTOOLCHAIN GOPROXY |
matches team defaults |
Settings that matter:
| Variable | Role | Persist with |
|---|---|---|
GOROOT |
installation root (inferred, rarely set) | fix the PATH instead |
GOPATH |
user workspace: module cache + tools | go env -w GOPATH=... |
GOBIN |
destination of go install (empty → $GOPATH/bin) |
go env -w GOBIN=... |
GOPROXY |
module download source | go env -w GOPROXY=... |
GOTOOLCHAIN |
auto (download as needed) / local (never) / pin |
go env -w GOTOOLCHAIN=local |
GOOS/GOARCH |
build target; cross-compile | per-command env var |
Code template:
# one-time setup check
go version && go env GOROOT GOTOOLCHAIN GOPROXY
# machine-specific config (survives shells and editors)
go env -w GOPROXY=https://proxy.corp.example,direct
# cross-compile from any host
go build -o bin/server ./cmd/server # with GOOS=linux GOARCH=amd64 set
Continue Learning
Before You Start
- No prerequisites listed.