Cross Compilation
GOOS and GOARCH tell go build which operating system and processor to target, so one machine can build for all of them.
GOOS=linux GOARCH=arm64 go build -o bin/app-linux-arm64 ./cmd/appCross compilation means building a program on one kind of machine for a different kind of machine. Your laptop might be an Apple computer with an ARM processor. The server is probably Linux on an Intel-compatible processor, which Go calls amd64. Go builds for that server from that laptop, with nothing extra to install.
Two variables
An environment variable is a named value the shell hands to a command. GOOS is the target operating system: linux, darwin (macOS), windows and others. GOARCH is the target processor family: amd64, arm64 and others. Run go env GOOS GOARCH to see what your own machine targets by default. Run go tool dist list to print every pair the toolchain supports, one per line.
Naming the output
Set both variables in front of the command, and use -o to choose the file name: GOOS=linux GOARCH=arm64 go build -o bin/app-linux-arm64 ./cmd/app. The argument ./cmd/app is shorthand for that package's full path, such as example.com/app/cmd/app; the two forms mean the same package and you can write either. Without -o, the file is named after the last component of the package's import path, and the go command adds the .exe suffix itself when it writes a Windows executable. -o, on the other hand, is taken literally, so when you name the file yourself for a Windows target you add .exe yourself.
What breaks
Cross compiling turns cgo off by default. Cgo is the mechanism that lets Go call C code; it has its own lesson. Any package that needs C will fail to build for the other platform until you supply a C cross compiler through the CC variable. Check the result with file bin/app-linux-arm64, which prints the operating system and processor the file was built for.
Example
// Command release prints the build command for every target we ship.
package main
import "fmt"
// target is one operating system and one processor family.
type target struct {
goos string // value for GOOS
goarch string // value for GOARCH
}
func main() {
targets := []target{
{goos: "linux", goarch: "amd64"},
{goos: "linux", goarch: "arm64"},
{goos: "windows", goarch: "amd64"},
}
for _, t := range targets {
out := fmt.Sprintf("bin/app-%s-%s", t.goos, t.goarch)
if t.goos == "windows" {
out += ".exe" // Windows will not run a file without this suffix
}
fmt.Printf("GOOS=%s GOARCH=%s go build -o %s ./cmd/app\n", t.goos, t.goarch, out)
}
}
// Output:
// GOOS=linux GOARCH=amd64 go build -o bin/app-linux-amd64 ./cmd/app
// GOOS=linux GOARCH=arm64 go build -o bin/app-linux-arm64 ./cmd/app
// GOOS=windows GOARCH=amd64 go build -o bin/app-windows-amd64.exe ./cmd/appWhen to use it
- Produce the Linux amd64 binary your cluster runs while developing on an Apple-silicon laptop.
- Publish downloadable binaries for Linux, macOS and Windows from a single release job.
- Build for a Raspberry Pi or an arm64 cloud instance without installing a toolchain on it.
More examples
Build for the server from your own machine
The first check matters because the variables only change the target for the command they are attached to. Everything else on your machine keeps building natively. Reach for this when a colleague or a server runs a different platform than you do and you want one machine to produce both binaries.
# What does this machine target when you say nothing?
go env GOOS GOARCH
# On an Apple-silicon Mac this prints:
# darwin
# arm64
# Build the same source for a 64-bit Linux server instead.
GOOS=linux GOARCH=amd64 go build -o bin/app-linux-amd64 ./cmd/app
# Confirm what came out.
file bin/app-linux-amd64
# bin/app-linux-amd64: ELF 64-bit LSB executable, x86-64, ...
# Every GOOS/GOARCH pair this toolchain can target, one per line.
go tool dist listA release target that loops over platforms
One command produces every artifact for a release. The double dollar signs are how a Makefile passes a dollar sign through to the shell, and $${p%/*} and $${p#*/} are the shell's way of cutting a string at the slash. Use this when you publish downloadable binaries and want the list of platforms written down in one place.
PLATFORMS := linux/amd64 linux/arm64 darwin/arm64
.PHONY: release
release:
for p in $(PLATFORMS); do \
GOOS=$${p%/*} GOARCH=$${p#*/} \
go build -trimpath -o bin/app-$${p%/*}-$${p#*/} ./cmd/app; \
doneCross compiling inside a Docker build
Building an arm64 image on an amd64 machine normally means emulating the whole toolchain, which is slow. Here the Go compiler cross compiles instead, so the build runs at native speed. Use this when one pipeline has to publish images for more than one processor.
# syntax=docker/dockerfile:1
# BUILDPLATFORM is the machine running the build; the builder stays native and fast.
FROM --platform=$BUILDPLATFORM golang:1.27 AS build
WORKDIR /src
COPY . .
# TARGETOS and TARGETARCH are filled in by the builder from --platform.
ARG TARGETOS
ARG TARGETARCH
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/app ./cmd/app
Discussion