Go Builds in WSL: Host Identity, Cross-Compilation, and cgo
Use Go in WSL without confusing the Linux build host with its target, and verify GOOS, GOARCH, cgo toolchains, module caches, and produced artifacts.
Go makes cross-compilation unusually approachable, but WSL adds an important distinction: the Go toolchain runs as a Linux process even though Windows is the physical host. The Linux guest architecture is the build host seen by Go; the requested output target is a separate choice. GOOS and GOARCH do not describe the computer where the command runs. They describe the environment the binary is meant to execute in.
That distinction matters when building command-line tools, release artifacts, and packages that use cgo. A Linux executable produced in WSL is not a Windows executable, even when the source and editor are on Windows. And setting two environment variables does not guarantee that every native dependency can be cross-compiled.
Verify the toolchain and its host
Install Go using the official Go distribution or a distribution package appropriate to the project. Then inspect the executable and effective environment in the WSL distro:
command -v go
go version
go env GOROOT GOPATH GOMODCACHE GOPROXY GOHOSTOS GOHOSTARCH GOOS GOARCH CGO_ENABLED
GOHOSTOS and GOHOSTARCH describe the toolchain host. GOOS and GOARCH describe the selected target, defaulting to the host unless overridden. Compare these values with the architecture of the WSL guest and the intended deployment. Do not infer the Linux target from the Windows CPU name alone, particularly on Windows-on-Arm systems.
Keep the module checkout and Go cache in the Linux filesystem for a Linux build. That avoids cross-filesystem metadata overhead and prevents Windows and Linux tools from producing artifacts in the same directory. If the Windows Go toolchain is also installed, keep its source builds and caches distinct.
Make a normal Linux build explicit
For a Linux service, a default build in the Linux environment is usually the simplest path:
cd ~/src/agent
mkdir -p ./out
go test ./...
go vet ./...
go build -o ./out/agent ./cmd/agent
file ./out/agent
The build result’s format should match the target. file is a useful inspection aid but not a complete compatibility test; it cannot prove that deployment libraries, kernel behavior, or runtime configuration are correct.
Keep build output under a known directory and ignore generated binaries if they are not source artifacts. Use a clean clone or remove only the project’s documented output directory when checking reproducibility. Do not delete the module cache merely because a build fails; first inspect the actual error and package resolution.
Cross-compile with an explicit target
Go’s command supports many OS and architecture combinations. The valid pairings are defined by the Go toolchain, so check the current official Go documentation rather than assuming every combination exists. For a target supported by the installed Go compiler:
mkdir -p ./out
GOOS=windows GOARCH=amd64 go build -o ./out/agent-windows-amd64.exe ./cmd/agent
GOOS=linux GOARCH=arm64 go build -o ./out/agent-linux-arm64 ./cmd/agent
The output’s operating system and architecture follow those environment variables, not the fact that the command executed in Linux. Name output files with their target to prevent a Windows artifact from replacing a Linux artifact or being packaged under an ambiguous name.
A cross-build can compile successfully without being runnable on the WSL host. Do not use a host go test result as evidence that a foreign-architecture binary passes runtime tests. Run target binaries in a matching runtime, emulator, or CI worker and record that test separately.
Treat cgo as a separate toolchain requirement
Pure Go code can often cross-compile using the Go compiler alone. When a package uses cgo, Go needs a C compiler and compatible target headers, libraries, linker, and sysroot. A host Linux GCC is not automatically a Windows cross-compiler, nor does setting GOARCH select a matching C toolchain.
Inspect whether cgo is enabled and which compiler Go will use:
go env CGO_ENABLED CC
go list -f '{{.CgoFiles}}' ./...
When a release does not require cgo and dependencies support it, CGO_ENABLED=0 can produce a pure-Go build for supported targets. It is not a universal workaround: packages may require cgo, behavior can change, and static linking assumptions must be tested. When cgo is necessary, install or configure an explicit cross C toolchain whose target ABI matches the Go target.
For native Linux builds that do use cgo, install the distro’s compiler and the development packages required by imported C libraries. Record those package names and versions alongside the Go version; the Go module graph does not lock system headers or shared libraries.
Keep Go modules reproducible
The Go module definition and checksum file are part of the build input. Review changes to go.mod and go.sum, use the project’s intended Go version, and test with a clean checkout. go mod download and go mod tidy have different purposes; do not run tidy as a generic repair command without reviewing the resulting dependency changes.
go mod download
go mod verify
go test ./...
git diff -- go.mod go.sum
The module cache is normally separate from the repository. It can be shared by projects using the same Linux user, but should not be copied from a Windows Go installation into WSL as though it were a portable artifact. If a private proxy is used, configure it intentionally and redact internal endpoints from reports.
Cross-compilation acceptance matrix
Define the expected release targets in source-controlled CI rather than relying on a developer’s remembered environment:
for target in "linux amd64" "linux arm64" "windows amd64"; do
set -- $target
os=$1
arch=$2
suffix=
if [ "$os" = windows ]; then suffix=.exe; fi
mkdir -p out
GOOS=$os GOARCH=$arch go build -o "out/agent-$os-$arch$suffix" ./cmd/agent
done
That simple loop is valid only when the package supports those target combinations and no cgo cross-toolchain is required. For a robust release script, validate each target against an explicit allowlist, use platform-correct output suffixes, and stop on the first nonzero command. Do not silently ignore unsupported targets.
Run host tests separately from cross-builds. If tests need to execute against the target, use a matching runner or emulator and make that dependency part of the acceptance record. A compile-only matrix catches type and build-tag errors; it is not integration testing.
Build constraints can change which files are selected for a target. A file guarded for Windows may be absent from a Linux build, while a Linux-only implementation may never compile on the Windows host. Ask the Go tool which files are selected for a target instead of inferring from filenames:
GOOS=linux GOARCH=arm64 go list -f '{{.GoFiles}} {{.CgoFiles}}' ./cmd/agent
GOOS=windows GOARCH=amd64 go list -f '{{.GoFiles}} {{.CgoFiles}}' ./cmd/agent
Package tests generally need to run on a compatible host. Some compile-only test commands can still reveal target-specific compile errors, but a cross-built test binary cannot be treated as executed merely because it was produced. Separate build-matrix evidence from test-matrix evidence in release notes.
If build tags or generated sources depend on the target, run generation under the documented environment and ensure the generated result is not accidentally committed from only one platform. Cross-compilation is especially useful for early detection, but platform-specific behavior still needs runtime coverage.
Diagnose common WSL-specific mistakes
If a generated binary reports “Exec format error,” inspect the binary format and the active target variables before changing permissions. If a linker reports missing libraries, determine whether the failure is from host cgo or a target sysroot. If a build behaves differently under Windows and WSL, record go version, go env, checkout path, and build command in both environments.
Go’s package build cache is keyed on inputs, but sharing source directories between Windows and Linux can still introduce path, case, permission, and generated-file differences. Keep one platform responsible for a given build output. Use separate output locations if a project deliberately builds for multiple OSes.
Acceptance checks
A WSL Go build is verified when the toolchain host values match the guest, target values match the release artifact, modules verify, tests pass on an executable target, and output inspection confirms the expected format. For cgo, record the selected C compiler and target libraries. For a cross-build, document which build was compile-only and which target received a runtime test.
These checks make the division explicit: WSL is the Linux build host, Go selects an output target, and deployment validation confirms that the artifact actually runs where intended.
Related:
- WSL on Windows Arm: Matching the Host, Kernel, Distro, and Packages
- WSL 1 or WSL 2: Choose by System-Call and Filesystem Workload
Sources: