A Go service fails in CI with go: go.mod requires go >= 1.23.1 (running go 1.22.5; GOTOOLCHAIN=local), while a teammate's build silently downloaded a different compiler; a Rust crate compiles on one laptop and fails on another with error[E0658]: use of unstable library feature because one developer is on nightly and the other on stable, and cargo clippy produces different warnings everywhere because each machine has a different Clippy version. Go and Rust both ship first-class, in-repository mechanisms for pinning the exact toolchain; most teams use neither fully. This page sets them up so every clone, CI job and image builds with the same compiler and components, as part of toolchain version management.

Unlike language runtimes managed by external tools, these pins are read by the language's own tooling — go and rustup — so they work without any extra installer beyond the base toolchain.

Diagnostic

Show what each ecosystem thinks the project requires and what it actually uses:

#!/usr/bin/env bash
set -euo pipefail
grep -E '^(go|toolchain) ' go.mod || echo "go.mod has no go/toolchain lines"
go version
go env GOTOOLCHAIN
cat rust-toolchain.toml rust-toolchain 2>/dev/null || echo "no rust-toolchain file"
rustup show active-toolchain 2>/dev/null || true
cargo clippy --version 2>/dev/null || true

Expected bad output:

go 1.22
go version go1.22.5 darwin/arm64
local
no rust-toolchain file
nightly-aarch64-apple-darwin (default)
clippy 0.1.83 (5d3c6ee9e3 2024-09-17)

go.mod sets only a minimum language version, GOTOOLCHAIN=local stops Go from fetching a newer compiler, and Rust falls back to whatever the developer set as their global default — here nightly.

What Each Pin Controls Table of the Go and Rust pinning mechanisms and what each one fixes. What Each Pin Controls Mechanism Pins Read by go.mod go line minimum language go command go.mod toolchain exact compiler go, GOTOOLCHAIN=auto rust-toolchain.toml channel and version rustup, cargo components list clippy, rustfmt rustup
Both ecosystems pin the compiler in the repository; Rust also pins components and targets.

Root cause

Go distinguishes two lines in go.mod. go 1.22 declares the minimum language version the module needs; toolchain go1.23.1 declares the exact toolchain to use. Since Go 1.21, the go command reads these and, with GOTOOLCHAIN=auto (the default), downloads and runs the required toolchain when the installed one is older — unless GOTOOLCHAIN=local was set, often in CI images or by corporate policy, in which case it fails instead. Without a toolchain line, every machine uses whatever Go is installed. Rust's rustup selects a toolchain per directory from rust-toolchain.toml; without that file it uses the user's global default, which may be stable, beta or nightly at any version. Clippy and rustfmt versions follow the toolchain, so lint and formatting results differ too. Neither ecosystem is at fault; the pin files simply were not written.

The cost of the missing pins is highest for linters and formatters. A newer Clippy adds lints and changes suggestions every release, and rustfmt occasionally changes formatting; gofmt is stable, but go vet checks evolve with the toolchain. Without pins, a developer who upgrades locally opens a pull request full of unrelated formatting changes, or CI fails with warnings nobody on the team can reproduce. With the toolchain pinned, lint output becomes a property of the repository rather than of whoever ran it, and upgrading the linter is a deliberate change with its own fix-up commit.

Resolution

  1. Pin the Go toolchain in go.mod:
#!/usr/bin/env bash
set -euo pipefail
go get [email protected]
go get [email protected]
grep -E '^(go|toolchain) ' go.mod

This writes go 1.23.0 (minimum language version) and toolchain go1.23.1 (exact compiler). Leave GOTOOLCHAIN at its default auto on developer machines, so an older installed Go transparently fetches 1.23.1.

  1. Pin the Rust toolchain, components and targets in rust-toolchain.toml:
[toolchain]
channel = "1.81.0"
components = ["rustfmt", "clippy", "rust-src"]
targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"]
profile = "minimal"

rustup installs exactly this toolchain the first time any cargo command runs in the directory, including Clippy and rustfmt at matching versions and the cross-compilation targets the project builds for.

  1. Use the same pins in CI and Docker. For Go, actions/setup-go reads the toolchain from go.mod; for Rust, rustup inside the job honours the file:
jobs:
  go:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version-file: go.mod
      - run: go build ./... && go vet ./...
  rust:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - run: rustup show active-toolchain
      - run: cargo fmt --check && cargo clippy -- -D warnings && cargo test

In Dockerfiles, base images should match the pin (golang:1.23.1-bookworm, rust:1.81.0-bookworm), or copy rust-toolchain.toml in before the first cargo call so rustup installs the pinned version.

  1. Check the pins in make doctor:
#!/usr/bin/env bash
set -euo pipefail
want_go=$(awk '$1=="toolchain"{print $2}' go.mod)
have_go=$(go env GOVERSION)
[ "$want_go" = "$have_go" ] && echo "go $have_go ok" || echo "go: want $want_go, have $have_go (check GOTOOLCHAIN)"
want_rs=$(awk -F'"' '/^channel/{print $2}' rust-toolchain.toml)
rustc --version | grep -q "$want_rs" && echo "rust $want_rs ok" || echo "rust: want $want_rs, have $(rustc --version)"
How the Go Command Picks a Toolchain Flow from reading go.mod through GOTOOLCHAIN to either using the local Go or downloading the pinned one. How the Go Command Picks a Toolchain read go.mod toolchain line local Go older? compare auto mode download pin build exact compiler
With GOTOOLCHAIN=auto the pin is enforced automatically; with local it fails loudly.

Expected output

$ go version
go version go1.23.1 darwin/arm64
$ rustup show active-toolchain
1.81.0-aarch64-apple-darwin (overridden by '/home/dev/src/shop/rust-toolchain.toml')
$ cargo clippy --version
clippy 0.1.81 (eeb90cda 2024-09-04)
$ ./scripts/doctor-toolchains.sh
go go1.23.1 ok
rust 1.81.0 ok

Every clone reports the same Go and Rust versions, rustup says the choice came from the repository file, and Clippy's version matches the pinned toolchain — so lint results are identical on every machine and in CI.

Developers who prefer nightly for personal experiments keep it as their global default; inside this repository, the pin file takes precedence, and rustup states that explicitly in its output. That removes a whole category of "works on my machine" reports caused by unstable features that only compile on nightly.

Prevention

  1. Update pins through reviewed pull requests. Renovate understands both the go.mod toolchain directive and rust-toolchain.toml, so upgrades arrive with a CI run proving them.

  2. Fail CI on unpinned repositories with a check that go.mod has a toolchain line and rust-toolchain.toml exists with a numeric channel, not stable.

  3. Avoid GOTOOLCHAIN=local in developer environments; reserve it for CI images that should fail rather than download, and make sure those images already contain the pinned version.

Floating vs Pinned Toolchains Comparison of relying on installed or default toolchains against repository pin files. Floating vs Pinned Toolchains installed or default pinned in repository differs per machine identical everywhere clippy results differ same lint version nightly leaks in channel fixed CI image decides CI reads same pin
The pin files make the compiler part of the repository, like a lockfile for tooling.

Platform caveats

Apple Silicon (ARM64): both toolchains publish native darwin/arm64 builds. Add Linux targets to rust-toolchain.toml if you cross-compile for containers, and set GOARCH/GOOS explicitly for Go cross-builds.

Corporate proxies: automatic Go toolchain downloads fetch from proxy.golang.org by default; set GOPROXY to the internal mirror, and configure RUSTUP_DIST_SERVER if rustup downloads are blocked.

Windows: rust-toolchain.toml and the Go toolchain directive work on native Windows; targets for Linux cross-compilation need a linker, so building in WSL2 or a container is usually simpler.

Rollback

Remove the pins; each machine returns to its installed or default toolchain:

#!/usr/bin/env bash
set -euo pipefail
go mod edit -toolchain=none
git rm -q rust-toolchain.toml

Frequently Asked Questions

What is the difference between the go and toolchain lines in go.mod?

The go line is the minimum language version the module needs. The toolchain line is the exact Go release to build with. With GOTOOLCHAIN=auto, the go command downloads that release if the installed one is older.

Why does CI say GOTOOLCHAIN=local and refuse to build?

The CI environment disables automatic toolchain downloads and its installed Go is older than the pin. Use actions/setup-go with go-version-file: go.mod, or a base image with the pinned version.

Should the Rust channel be stable or a version number?

A version number. stable moves every six weeks, so two machines on "stable" can differ. Pin 1.81.0 and update deliberately.

Do the pins slow down first builds?

Only once per version per machine: Go and rustup download the toolchain the first time and reuse it afterwards. In CI, cache the toolchain directories to avoid repeated downloads.