The version mismatch trap
You clone a repository from a colleague. You run go run main.go. The compiler rejects the build because the code uses slices.SortFunc, a function added in Go 1.21. Your system is running Go 1.20. You need to upgrade, but your other projects depend on 1.20 for stability. Reinstalling the system compiler breaks your workflow. You need a way to swap Go versions per project without fighting with symlinks or environment variables.
goenv solves this by managing multiple Go installations and switching between them automatically. It does not manage modules or dependencies. It manages the compiler binary. You install several versions of Go. goenv intercepts your go commands and routes them to the correct binary based on your current directory or global configuration.
How goenv intercepts your commands
goenv works through shell shims. A shim is a lightweight script that sits between your terminal and the real binary. When you install goenv, it places a directory containing fake scripts named go, gofmt, and godoc at the front of your PATH.
When you type go version, your shell searches the PATH for an executable named go. It finds the goenv shim first. The shim does not compile anything. It checks for a file named .go-version in your current directory. If that file exists, the shim reads the version inside it. If not, it checks the GOENV_VERSION environment variable. If that is empty, it falls back to the global default. Once the shim identifies the target version, it locates the real Go binary in ~/.goenv/versions/<version>/bin/go and executes it.
This mechanism means goenv does not modify the Go binary. It modifies your shell environment. The real Go installation lives in ~/.goenv/versions. The shim is the dispatcher.
Shims are invisible until they break. Check your PATH order first.
Installing and switching versions
Install goenv using your system package manager or from source. On macOS, Homebrew is the standard path. On Linux, download the repository and follow the installation script. After installation, you must initialize goenv in your shell profile. This step adds the shim directory to your PATH and sets up shell hooks.
Here is the standard initialization for zsh or bash. Add these lines to ~/.zshrc or ~/.bashrc.
# Prepend goenv to PATH so shims are found before system binaries
export PATH="$HOME/.goenv/bin:$PATH"
# Initialize goenv shell integration. Must be at the end of the file.
eval "$(goenv init -)"
The eval command runs the output of goenv init - in your current shell. This command rewrites your PATH and sets up the goenv shell function. If you place this before other PATH exports, the system Go might take precedence. Put it at the end of your shell config file.
Once initialized, you can install a specific Go version. goenv downloads the official Go release, extracts it, and places it in the versions directory.
# Download and install Go 1.21.0 into ~/.goenv/versions
goenv install 1.21.0
# Set 1.21.0 as the global default for all directories
goenv global 1.21.0
The global command writes the version to ~/.goenv/version. This file acts as the fallback when no local version is specified. You can verify the active version with goenv version.
Project isolation with .go-version
Global settings apply to your entire machine. Project-specific settings apply only to a single directory tree. goenv uses a file named .go-version to track local versions. This file lives in the root of your project. When you enter the directory, the shim reads the file and switches the Go binary.
Navigate to a project directory and set the local version.
cd ~/projects/legacy-service
# Install Go 1.20.5 if not already installed
goenv install 1.20.5
# Create .go-version file in this directory with content "1.20.5"
goenv local 1.20.5
The local command creates or updates the .go-version file. You can now run go version inside this directory and see 1.20.5. Run it in your home directory and see 1.21.0. The switch happens automatically. You do not need to reload your shell.
Commit the .go-version file to version control. It documents the Go version required by the project. Other developers who clone the repo and have goenv installed will automatically switch to the correct version when they enter the directory.
The .go-version file is the source of truth for the project. Commit it.
The go.mod toolchain directive
Go 1.21 introduced the toolchain directive in go.mod. This directive allows a module to specify the exact toolchain version used to build it. The first line of go.mod declares the minimum Go version. The toolchain line declares the specific version.
module example.com/myapp
go 1.21
toolchain go1.21.3
When you run go build, the compiler checks the toolchain directive. If the directive specifies a version and the GOTOOLCHAIN environment variable is set to auto (the default), Go will download the specified toolchain if it is missing. This behavior can conflict with goenv.
If you use goenv, you likely want to force Go to use the version managed by goenv. Set GOTOOLCHAIN=local in your shell environment. This tells Go to use the currently active toolchain and refuse to download others.
# Force Go to use the active goenv version and skip auto-download
export GOTOOLCHAIN=local
With GOTOOLCHAIN=local, if your go.mod requires go1.21.3 and goenv is set to 1.21.0, the compiler rejects the build. The error message is explicit: go: go.mod requires go1.21.3 but go version is go1.21.0. You must switch to the correct version using goenv local or goenv global.
This setup keeps your environment predictable. goenv controls the binary. GOTOOLCHAIN=local prevents silent downloads. The compiler enforces the match.
GOTOOLCHAIN controls the download behavior. Set it to local to trust goenv.
Pitfalls and shell quirks
goenv relies on your shell configuration. If the initialization fails, the shims do not load, and you fall back to the system Go. Common issues include incorrect PATH ordering and missing shell hooks.
If go version shows the system version instead of the goenv version, check your PATH. Run echo $PATH. The first directory should be $HOME/.goenv/shims. If it is not, goenv is not intercepting commands. Move the eval "$(goenv init -)" line to the end of your shell config and restart your terminal.
Another issue is the shim cache. goenv maintains a cache of shims for performance. When you install a new version, goenv usually updates the cache automatically. If you manually modify files in ~/.goenv/versions, run goenv rehash to rebuild the shim cache.
# Rebuild the shim cache after manual changes to versions directory
goenv rehash
Do not set GOROOT manually. goenv manages GOROOT for each version. If you export GOROOT in your shell profile, it overrides goenv's internal logic and breaks version switching. Remove any export GOROOT lines from your shell config.
goenv does not manage GOPATH. GOPATH is independent of the Go version. You can use the same GOPATH across all versions. The default GOPATH is ~/go. This directory stores modules and binaries. Switching Go versions does not clear your module cache. Binaries installed via go install are cached per version in ~/go/bin. If you switch versions and run go install, the new binary is placed in the same directory. If two versions produce binaries with the same name, the last one wins. Use go env GOCACHE to check the cache location.
The worst goroutine bug is the one that never logs. The worst goenv bug is the one that silently uses the wrong compiler. Verify with go version.
Decision matrix
Use goenv when you need per-project Go version isolation on your local machine and want a simple, language-specific tool.
Use asdf when you manage multiple languages like Node.js, Python, and Go, and prefer a single plugin-based system for all runtimes.
Use Docker when you need reproducible builds that match the CI environment exactly, including OS-level dependencies and system libraries.
Use the system package manager when you work on a single project, do not need to switch versions, and prefer minimal tooling overhead.
Trust gofmt. Argue logic, not formatting. Trust goenv. Argue versions, not PATH.