Building and deploying
Produce optimised Go binaries with build flags, cross-compile for any platform, create static binaries, build Docker images with multi-stage builds, and use go generate.
- Use -ldflags to embed version information and strip debug symbols
- Cross-compile a binary for a different OS/architecture with GOOS and GOARCH
- Produce a fully static binary with CGO_ENABLED=0
- Write a Docker multi-stage build for a minimal Go image
- Use go generate to automate code generation
One of Go's distinctive strengths is its compilation model: a single go build produces a self-contained binary with no runtime dependency. No interpreter, no VM, no shared libraries (by default). This makes deployment to containers, bare metal, and serverless environments simple and reliable. Knowing the build system well lets you produce smaller, faster, more auditable binaries.
go build flags
The most useful production build flags are in -ldflags:
go build -ldflags="-s -w" -o myapp .-s— omit the symbol table (reduces binary size).-w— omit DWARF debug information (further size reduction).
Together they typically cut binary size by 30–50%. You lose stack trace detail in production panic logs — weigh this against size constraints.
Embedding version information
Use -ldflags to bake the version, commit hash, and build time into the binary at build time:
// version.go
package main
var (
Version = "dev"
Commit = "none"
BuildTime = "unknown"
)go build \
-ldflags="-X main.Version=1.2.3 -X main.Commit=$(git rev-parse --short HEAD) -X main.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-o myapp .The -X pkg.Var=value flag overwrites a string variable at link time. The result: myapp --version can print 1.2.3 (abc1234, built 2025-06-09T10:00:00Z) without any runtime configuration.
Cross-compilation
Go can compile for any supported platform by setting two environment variables:
# Build a Linux amd64 binary on macOS or Windows
GOOS=linux GOARCH=amd64 go build -o myapp-linux .
# Build for ARM (e.g., Raspberry Pi)
GOOS=linux GOARCH=arm64 go build -o myapp-arm64 .
# Build for Windows
GOOS=windows GOARCH=amd64 go build -o myapp.exe .No toolchain installation required — the Go compiler handles everything. Check the full list of supported platforms:
go tool dist listCGO (C bindings) breaks cross-compilation unless you have the right C cross-compiler installed. If you use database/sql with a pure-Go driver, you don't need CGO. Set CGO_ENABLED=0 to get a fully static binary with zero C dependency.
Static binaries
CGO_ENABLED=0 GOOS=linux go build -o myapp .
file myapp
# myapp: ELF 64-bit LSB executable, statically linkedA static binary contains every dependency — no shared libraries on the target machine required. This is ideal for containers: you can copy just the binary into an empty FROM scratch image.
Docker multi-stage builds
A multi-stage Dockerfile builds in one image and runs in another, keeping the final image minimal:
# Stage 1: build
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o myapp .
# Stage 2: run
FROM scratch
COPY --from=builder /app/myapp /myapp
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/myapp"]The final image contains only the binary and TLS certificates (needed for HTTPS calls). Typical size: 5–15 MB, compared to 500–800 MB for a golang base image.
The FROM scratch image has no shell, no ls, no /etc/passwd. Debugging requires attaching to a running container or switching to FROM gcr.io/distroless/static for a minimal-but-debuggable base. Use distroless in development, scratch for maximum production minimalism.
go generate
go generate runs arbitrary commands embedded in Go source files:
//go:generate stringer -type=Status
//go:generate protoc --go_out=. proto/api.protoRun with:
go generate ./...Common uses:
stringer— generateString()methods foriotaenum types.mockgen— generate interface mocks for testing.protoc— generate gRPC stubs.sqlc— generate type-safe database code from SQL.
go generate is not run automatically by go build — you run it explicitly, then commit the generated files. This keeps builds reproducible without requiring the generator tools at build time.
The -trimpath flag
go build -trimpath -o myapp .-trimpath removes all local filesystem paths from the binary. Without it, panic stack traces contain the absolute path of your source files (e.g., /home/alice/projects/myapp/main.go). With it, paths are module-relative. Use it in production builds for both privacy and reproducible binaries.
Check your understanding
Knowledge check
- 1.What does -ldflags="-s -w" do to a Go binary?
- 2.Setting CGO_ENABLED=0 is required to produce a fully static Go binary with no shared library dependencies.
- 3.When does go generate run?
Do it yourself
Build your service with version embedding and verify the result:
go build \
-ldflags="-s -w -X main.Version=0.1.0 -X main.Commit=$(git rev-parse --short HEAD)" \
-o myapp .
./myapp --version # should print 0.1.0 (abc1234)
ls -lh myapp # note the binary sizeThen try cross-compilation:
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o myapp-arm64 .
file myapp-arm64Where to go next
You've reached the end of the Production Go module and the Go advanced track. You can now build, profile, test, and deploy Go services. From here, explore the Go standard library documentation, the official Go blog, and production-grade patterns in open-source Go projects to continue your journey.