The architecture mismatch trap
You just unboxed a MacBook with an M-series chip. You're ready to write Go. You copy a tutorial command from a blog post written three years ago, paste it into the terminal, and hit enter. The shell responds with exec format error. Or you install Go via a package manager, and your editor complains it can't find the language server.
Apple Silicon changed the hardware landscape. Intel Macs used x86-64 architecture. M1, M2, M3, and M4 chips use ARM64. Go is a compiled language. The compiler produces machine code that runs directly on the CPU. If you grab the wrong binary, the CPU doesn't understand the instructions. Rosetta 2 can translate x86 binaries to ARM, but running a translated Go toolchain causes subtle bugs, performance hits, and confusion.
Setting up Go on Apple Silicon requires picking the darwin-arm64 distribution, placing it in a consistent location, and configuring your shell so every tool can find it. Go is self-contained. It doesn't scatter headers and libraries across your system. It lives in one directory. Your job is to point the system to that directory.
How the toolchain lives on disk
Go distributes itself as a tarball. This is a compressed archive containing the entire toolchain. Inside the archive, you find a directory named go. That directory holds everything: the compiler, the linker, the standard library source code, and helper tools.
The official convention is to extract this to /usr/local/go. The result is /usr/local/go/bin, which contains the go executable. When you run go version, the shell searches the directories listed in the PATH environment variable. If /usr/local/go/bin is in that list, the shell finds the binary and executes it.
The Go binary detects its own installation path. It sets GOROOT automatically based on where the executable lives. You rarely need to set GOROOT manually. The toolchain uses GOROOT to locate the standard library. If GOROOT is wrong, the compiler rejects your code with cannot find package fmt or similar errors because it can't find the built-in packages.
Your workspace lives elsewhere. By default, Go uses $HOME/go as the GOPATH. This is where your source code and downloaded modules go. GOBIN defaults to $GOPATH/bin. When you run go install, the resulting binary lands in GOBIN. If GOBIN isn't in your PATH, you can't run tools installed via go install without typing the full path.
Go keeps the toolchain and your workspace separate. The toolchain is immutable. Your workspace changes constantly. This separation makes updates safe. You can delete /usr/local/go and drop in a new version without touching your code.
The installation sequence
Here's the standard installation sequence. Download the archive, extract it to the system directory, and update your shell configuration.
# Download the official tarball for Apple Silicon
# darwin-arm64 matches the M1/M2/M3/M4 architecture
# -L follows redirects, -O saves with the original filename
curl -LO https://go.dev/dl/go1.22.4.darwin-arm64.tar.gz
# Extract to /usr/local so the binary lands in /usr/local/go/bin
# sudo is required because /usr/local is owned by root
# -C changes the target directory before extracting
sudo tar -C /usr/local -xzf go1.22.4.darwin-arm64.tar.gz
# Append the Go bin directory to PATH so the shell finds the 'go' command
# zsh is the default shell on macOS; adjust for bash if needed
# >> appends to the file without overwriting existing content
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.zshrc
# Re-read the config file so the current session sees the change
source ~/.zshrc
The curl command fetches the file. The -L flag tells curl to follow HTTP redirects. The Go website often redirects to a CDN. Without -L, curl might save a 302 redirect page instead of the tarball. The -O flag saves the file using the remote name. This makes the filename predictable for the next step.
The tar command extracts the archive. The -x flag extracts, -z decompresses gzip, and -f specifies the file. The -C /usr/local flag is critical. It tells tar to change to /usr/local before extracting. The archive contains a top-level go directory. Extracting to /usr/local creates /usr/local/go. If you omit -C, tar extracts into the current directory, creating ./go. You'd have to move it manually.
The echo command appends the PATH export to ~/.zshrc. macOS uses zsh as the default shell. The export command makes the variable available to child processes. The $PATH expansion preserves existing directories. The shell searches PATH from left to right. Appending puts Go at the end. This is usually safe. If you have an older Go installation earlier in PATH, the shell might find that one first. You can prepend /usr/local/go/bin:$PATH to prioritize the new installation.
The source command re-reads ~/.zshrc. Without this, the current terminal session doesn't see the new PATH. You'd have to close and reopen the terminal. source applies the change immediately.
Verifying the environment
Once the installation is complete, you need to verify the toolchain is working and reporting the correct architecture.
# Check the version and build information
# This confirms the binary runs and reports the target architecture
go version
# Inspect environment variables the toolchain is using
# GOROOT should point to /usr/local/go
# GOPATH should default to $HOME/go
# GOOS and GOARCH confirm the host platform
go env GOROOT GOPATH GOOS GOARCH
The output of go version should show darwin/arm64. If it shows darwin/amd64, you installed the wrong binary. The output of go env reveals the configuration. GOOS=darwin means the operating system is macOS. GOARCH=arm64 means the architecture is ARM. These values determine how the compiler generates code.
If GOROOT is empty or points to the wrong location, the toolchain is misconfigured. The official binary usually sets this correctly. If GOPATH is set to something unexpected, check your shell configuration. Some users set GOPATH globally, which can interfere with module-aware workflows. Modern Go prefers the default $HOME/go.
Go comes with gofmt. The community treats formatting as a solved problem. Don't argue about indentation. Configure your editor to run gofmt on save. The tool decides. This convention saves time and keeps codebases consistent. You'll use gofmt constantly. It's part of the setup culture.
Common pitfalls and compiler errors
Setup issues manifest in specific ways. Knowing the error messages helps you diagnose the problem quickly.
If you forget to add Go to PATH, the shell can't find the command. You get zsh: command not found: go. This isn't a Go error. It's a shell error. The binary exists, but the shell doesn't know where to look. Check echo $PATH to see if /usr/local/go/bin is present.
If you install the amd64 binary on an M-series Mac, the kernel refuses to execute it. You get exec format error or Bad CPU type in executable. Rosetta 2 might intercept this and try to translate, but the Go toolchain contains multiple binaries and scripts. Translation often fails or produces broken behavior. Always use darwin-arm64.
If you extract the tarball to the wrong directory, GOROOT might be wrong. The compiler rejects imports with cannot find package fmt. This happens when the toolchain can't locate the standard library. Run go env GOROOT to check. If it's wrong, move the go directory to /usr/local/go or set GOROOT manually.
Homebrew installs Go to /opt/homebrew/opt/go/libexec. This path differs from /usr/local/go. If you mix Homebrew and manual installations, you can end up with two Go toolchains. The shell might find one, but your editor might find the other. This causes version mismatches. Pick one method. Consistency beats cleverness.
If you run go install but can't find the resulting binary, check GOBIN. The tool puts the binary in GOBIN. If GOBIN isn't in PATH, you have to type the full path. Add export PATH=$PATH:$HOME/go/bin to your shell config to fix this.
The shell doesn't remember. Source the file or restart the terminal.
Realistic workflow: installing a tool
A working setup lets you install third-party tools. The Go ecosystem relies on go install for tooling. This command downloads the source, compiles it, and places the binary in GOBIN.
# Install the official language server for editor support
# gopls provides autocomplete, diagnostics, and navigation
# The @latest suffix fetches the newest version
go install golang.org/x/tools/gopls@latest
# Verify the tool is accessible
# If this fails, GOBIN is not in PATH
gopls version
This workflow assumes GOBIN is in PATH. If gopls version fails with command not found, the binary is in $HOME/go/bin, but the shell can't find it. Update your PATH to include $HOME/go/bin.
Editor integration depends on this. VS Code, Neovim, and other editors use gopls to provide Go features. If the editor can't find gopls, you lose autocomplete and error checking. The editor usually searches PATH. Ensure your shell configuration matches the environment the editor uses.
Modern Go uses modules. You don't need to manage GOPATH manually for dependencies. Run go mod init in your project directory to start a module. The toolchain handles the rest. This is the standard workflow. Stick to modules. Avoid the old GOPATH mode unless you're maintaining legacy code.
Decision: installation methods
Several methods exist for installing Go. Each has trade-offs.
Use the official tarball when you want a system-wide installation that matches the Go team's tested configuration. This method puts Go in /usr/local/go, which is the expected location for many tools and tutorials. It requires manual updates, but it's transparent and reliable.
Use Homebrew when you prefer package management and don't mind the installation landing in /opt/homebrew. Homebrew handles updates automatically. It integrates with other tools. The path differs from the official convention, which can cause confusion if you follow tutorials that assume /usr/local/go.
Use goenv when you need to switch between multiple Go versions frequently for different projects. goenv manages shims and lets you set a version per directory. This adds complexity. It's useful for compatibility testing. Most developers don't need multiple versions.
Use a containerized workflow when you want to isolate the Go toolchain from your host system entirely. Docker images with Go pre-installed provide a consistent environment. This is ideal for CI/CD pipelines. It's overkill for local development unless you're already using containers for everything.
Pick one method. Consistency beats cleverness.