Distribution & Cross-Compilation
Go compiles to a single static binary with no runtime dependencies. You can build for any platform from any platform with two environment variables. This makes distribution trivial: copy the binary, run it. No interpreter to install, no library versions to manage, no Docker required (though Docker works beautifully with Go too).
GOOS & GOARCH
Cross-compilation in Go requires no extra toolchains. Set GOOS and GOARCH:
# Build for Linux (amd64) from macOS
GOOS=linux GOARCH=amd64 go build -o myapp-linux-amd64 ./cmd/myapp
# Build for Windows
GOOS=windows GOARCH=amd64 go build -o myapp-windows-amd64.exe ./cmd/myapp
# Build for Linux ARM (Raspberry Pi, AWS Graviton)
GOOS=linux GOARCH=arm64 go build -o myapp-linux-arm64 ./cmd/myapp
# Build for macOS Apple Silicon
GOOS=darwin GOARCH=arm64 go build -o myapp-darwin-arm64 ./cmd/myapp
Common platform combinations:
GOOS GOARCH Description
------------------------------------------
linux amd64 Most servers, CI/CD
linux arm64 AWS Graviton, ARM servers
darwin amd64 Intel Mac
darwin arm64 Apple Silicon Mac
windows amd64 Windows desktop/server
See all supported platforms:
go tool dist list
Go supports over 40 OS/architecture combinations. You can build for FreeBSD, OpenBSD, Plan 9, WebAssembly, and more.
Static Binaries
By default, Go produces statically linked binaries on most platforms. No shared libraries, no runtime dependencies:
go build -o myapp ./cmd/myapp
file myapp
# myapp: ELF 64-bit LSB executable, x86-64, statically linked
ldd myapp
# not a dynamic executable
If your code uses CGo (C bindings), the binary may link dynamically. Disable CGo for a fully static binary:
CGO_ENABLED=0 go build -o myapp ./cmd/myapp
This is important for Alpine Linux and scratch Docker images, which do not have glibc.
Embedding Version Information
Use -ldflags to embed build-time variables:
// main.go
package main
var (
version = "dev"
commit = "none"
date = "unknown"
)
func main() {
if len(os.Args) > 1 && os.Args[1] == "version" {
fmt.Printf("myapp %s (commit %s, built %s)\n", version, commit, date)
return
}
// ...
}
Set the values at build time:
go build -ldflags "-X main.version=1.2.3 -X main.commit=$(git rev-parse --short HEAD) -X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" -o myapp ./cmd/myapp
$ myapp version
myapp 1.2.3 (commit a1b2c3d, built 2025-01-15T10:30:00Z)
goreleaser: Automated Releases
goreleaser automates the entire release process: cross-compilation, checksums, changelogs, and publishing to GitHub Releases:
# .goreleaser.yaml
version: 2
builds:
- main: ./cmd/myapp
binary: myapp
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
ldflags:
- -s -w
- -X main.version={{.Version}}
- -X main.commit={{.Commit}}
- -X main.date={{.Date}}
archives:
- format: tar.gz
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
format_overrides:
- goos: windows
format: zip
checksum:
name_template: "checksums.txt"
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
Release with a git tag:
git tag v1.2.3
git push origin v1.2.3
goreleaser release
In CI (GitHub Actions):
# .github/workflows/release.yaml
name: Release
on:
push:
tags:
- "v*"
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: stable
- uses: goreleaser/goreleaser-action@v6
with:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Docker: scratch Base Image
Go's static binaries are perfect for Docker. The binary IS the container:
# Build stage
FROM golang:1.23-alpine AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /myapp ./cmd/myapp
# Runtime stage
FROM scratch
COPY --from=build /myapp /myapp
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/myapp"]
The scratch image is literally empty: no shell, no OS, no package manager. Your final image contains only the binary and CA certificates (needed for HTTPS).
Image Size
----------------------------
golang:1.23 800MB
ubuntu:24.04 80MB
alpine:3.19 7MB
scratch + Go bin ~10-15MB
If you need a shell for debugging, use alpine instead of scratch:
FROM alpine:3.19
RUN apk --no-cache add ca-certificates
COPY --from=build /myapp /myapp
ENTRYPOINT ["/myapp"]
Reduce Binary Size
The -ldflags="-s -w" flags strip debug information and symbol tables:
# Without stripping
go build -o myapp ./cmd/myapp # ~15MB
# With stripping
go build -ldflags="-s -w" -o myapp ./cmd/myapp # ~10MB
For even smaller binaries, use upx compression (trades startup time for size).
Distributing via go install
If your tool is open source, users can install it directly:
go install github.com/yourname/myapp@latest
This downloads the source, compiles it for the user's platform, and puts the binary in $GOPATH/bin. It is the standard way to distribute Go CLI tools.
For this to work, your module must be tagged with semantic versions:
git tag v1.0.0
git push origin v1.0.0
Users can install specific versions:
go install github.com/yourname/myapp@v1.2.3
Build Scripts
For projects with multiple binaries or complex build steps, use a Makefile:
VERSION := $(shell git describe --tags --always --dirty)
COMMIT := $(shell git rev-parse --short HEAD)
DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS := -s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DATE)
.PHONY: build
build:
CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -o bin/myapp ./cmd/myapp
.PHONY: build-all
build-all:
GOOS=linux GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o bin/myapp-linux-amd64 ./cmd/myapp
GOOS=linux GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o bin/myapp-linux-arm64 ./cmd/myapp
GOOS=darwin GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o bin/myapp-darwin-arm64 ./cmd/myapp
GOOS=windows GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o bin/myapp-windows-amd64.exe ./cmd/myapp
.PHONY: docker
docker:
docker build -t myapp:$(VERSION) .
Common Pitfalls
- Forgetting CGO_ENABLED=0 for Docker scratch images. If any dependency uses CGo, the binary links dynamically and fails on scratch/alpine. Always set
CGO_ENABLED=0. - Not embedding version info. Users need
myapp versionto report bugs. Always embed version, commit, and build date. - Shipping debug binaries in production. Use
-ldflags="-s -w"to strip debug info. The binary is 30-40% smaller. - Not including CA certificates in scratch images. Without
/etc/ssl/certs/ca-certificates.crt, your binary cannot make HTTPS requests. - Building for the wrong architecture. ARM servers (Graviton, M-series) need
GOARCH=arm64, notamd64. CI defaults are usually amd64. - Not tagging releases for go install. Without semantic version tags,
go install @latestdoes not work.
Key Takeaways
- Cross-compilation requires only
GOOSandGOARCH. No extra toolchains needed. CGO_ENABLED=0ensures a fully static binary with no system dependencies.- Use
-ldflagsto embed version, commit, and build date at compile time. goreleaserautomates cross-compilation, checksums, and GitHub Releases.- Docker scratch images with a Go binary produce containers under 15MB.
go installis the standard distribution method for open-source Go CLI tools.- Cross-compilation to any platform from any platform is one of Go's most practical advantages.