skip to content

Your onboarding script installs Go tools with go install, and every machine ends up with a different build. Why, and how do you fix it?

level: seniorimportance: should knowfreq 42%

answer

  1. the script is fixed; its argument is not
  2. a query, not a version
  3. your go.mod records nothing about it
  4. check where the go command writes
  5. one reviewed list of exact versions

basics

~20 s

Almost always the script uses @latest, which resolves to whatever the newest release is the moment each machine runs it, and nothing in the repository records what was installed. Pin an exact version per tool, and fix the install destination explicitly.

solid answer

~50 s

Two things drift. First, `go install example.com/cmd/mytool@latest` resolves the version **at run time**, so a machine set up in March and one set up in June get different builds from the identical script. Second, the version form ignores your `go.mod` entirely — installing a tool records nothing in the repository, so there is no artefact anyone can diff or review. The fix is to make both explicit: one reviewed list of `[email protected]` lines with exact semantic versions, never `@latest`, changed only in a commit; and a deliberate destination, by setting `GOBIN` in the script rather than inheriting whatever `$GOPATH/bin` happens to be. When someone reports "command not found" after running it, start with `go env GOBIN GOPATH` — the binary is nearly always exactly where the go command said and simply not on PATH. `go install -n` prints what a step would do without doing it.

code

text · 4 lines
text
$ export GOBIN="$PWD/.bin"
$ go install example.com/cmd/[email protected]   # never @latest in a script
$ go env GOBIN GOPATH                       # print it, so logs are diagnosable
$ command -v mytool                         # is the new copy the one on PATH?

go deeper

for a junior

Know that @latest means whatever is newest right now, so two people running the same script can get different builds, and that an exact version suffix removes that.

for a middle

Explain why the repository holds no record of the installed tool — the version form of go install is isolated from your module — and where the binary actually lands.

for a senior

Demonstrate the diagnosis in order: confirm the destination with go env, confirm which copy PATH resolves, then compare versions; and fix the shared script rather than the machine in front of you.

for a principal

Own the standard: one reviewed place where tool versions are named, an explicit install destination, and an upgrade that arrives as a reviewable commit rather than as drift nobody can date.

## The symptom A repository has a `setup.sh` that a newcomer runs on day one. It installs a handful of developer commands with `go install`. Six months in, no two laptops agree: one person's generator emits different output, another's checker reports findings nobody else sees, and the script has not changed in either case. The script is deterministic; the thing it invokes is not. ## Cause one: @latest is resolved when it runs `go install example.com/cmd/mytool@latest` asks, at that moment, for the newest release of that module. The answer depends entirely on the calendar. Every machine gets "the latest", which is not a version — it is a query. Two developers running byte-identical scripts weeks apart install different software, and neither script nor repository says which. The fix is unglamorous and complete: **name the version**. ``` go install example.com/cmd/[email protected] ``` Now the version is text in a file. It is greppable, reviewable in a pull request, and it changes only when someone decides it should. Upgrading becomes a commit — with the behaviour change attributable to it — instead of an invisible consequence of when you joined the team. ## Cause two: nothing in the project records the tool The `pkg@version` form of `go install` deliberately builds in an isolated module context: it does not read your `go.mod`, does not write it, and takes its dependency versions from the tool module's own requirements. That isolation is what stops a code generator from bumping a library in your service — genuinely valuable — but it has a direct consequence: **your repository knows nothing about the tools you installed**. There is no lock file for them and no command that reports "the versions this project expects". So the recording has to be something you create. A single file listing every tool and its exact version, consumed by the setup script, is enough. What matters is that exactly one place names each version and that a reader can find it. ## Cause three: the destination is inherited, not chosen `go install` writes to `$GOBIN`, or `$GOPATH/bin` when GOBIN is empty, defaulting to `~/go/bin`. Across a team, some machines have GOBIN set to something bespoke, some have an old GOPATH from a pre-modules era, and some have a stale binary of the same name earlier on PATH. The result is a script that appears to succeed while the command a developer actually runs comes from somewhere else. Make it explicit in the script: - set `GOBIN` yourself so every machine lands in the same place; - print `go env GOBIN GOPATH` in the script's output, so a failed setup is diagnosable from a pasted log; - have the script check that the destination is on PATH and say so plainly if it is not, rather than leaving the newcomer with `command not found`. A repository-local bin directory works well: it cannot collide with anything the developer installed for other work, and deleting it resets the environment completely. ## Diagnosing a machine that is already wrong - `go env GOBIN GOPATH` — where the go command will write, which is the first question to settle. - The shell's own lookup (`command -v mytool`) — whether the binary being run is the one that was just installed or an older copy earlier on PATH. - `go install -n <the exact line from the script>` — prints the commands the go command would run without running them, which shows whether the step is doing what the script author believed. The order matters: confirm the destination, then confirm which copy is being executed, and only then argue about versions. ## What good looks like An onboarding script whose tool section is one loop over a reviewed list of `module/[email protected]` lines; an explicit `GOBIN` that the script also verifies is on PATH; and a printed summary of what it installed and where. Re-running it on an existing machine produces the same result as running it on a fresh one — which is the property you are actually testing when you wipe a laptop and follow your own README. ## The judgement being assessed Anyone can say "pin the version". The senior signal is knowing **why** the pin has to live in your repository rather than being derivable from it: the version form of `go install` is isolated from your module by design, so reproducibility here is something you build, not something the go command gives you. The second signal is treating the install destination and PATH as part of the contract, because a correct install nobody can execute is indistinguishable from a failed one.

  • Why doesn't the repository's go.mod pin the version of a tool installed with the @version form?
    Because that form builds in an isolated module context: it neither reads nor writes the main module's `go.mod` and `go.sum`. The isolation is deliberate — a tool's dependencies can never perturb your service's — but it means nothing in the repository records the tool. If you want that recorded, you have to write it down somewhere yourself and have the script read it.
  • A newcomer runs the script successfully, then gets command not found. Where do you look first?
    `go env GOBIN GOPATH`. The binary is almost always exactly where the go command reported — `$GOBIN`, or `$GOPATH/bin` defaulting to `~/go/bin` — and that directory is not on their PATH. The second check is `command -v` for the tool, which catches the opposite failure: an older copy of the same name shadowing the freshly installed one.
  • How do you see what a go install line in the script would do without installing anything?
    Add `-n`: the go command prints the commands it would run and executes none of them. It is the quickest way to confirm a step is really building what the author intended, and it pairs well with printing `go env GOBIN GOPATH` in the same dry run so the destination is visible in the log rather than assumed.

Handing every newcomer a recipe that says "buy the freshest bread" instead of naming the loaf: the instructions match, the sandwiches do not.

saying these in an interview costs you the question

  • Treats @latest as acceptable in a setup script
  • Assumes the project's go.mod pins installed tool versions
  • Blames the go command rather than an unpinned version query
  • Never checks GOBIN or GOPATH before debugging PATH
  • Fixes one laptop by hand instead of the script everyone runs