Cross-Compiling Go

Updated

September 12, 2026

Cross-Compiling Go

Go cross-compiles without a sysroot when CGO is off. The boring default is: CGO_ENABLED=0 and GOOS/GOARCH via buildGoModule env, or a native remote builder — not qemu-user for every PR.

pkgsCross still matters if CGO is on (cgo + glibc for the target). For the desk API, turn CGO off first. This is not the C pkgsCross chapter: the compiler is still the build go; it merely emits another GOARCH.

Mental model

Need Tool
linux/amd64 binary from a Mac GOOS=linux GOARCH=amd64, CGO=0
linux/arm64 on x86_64, CGO=0 GOARCH=arm64
CGO + glibc for aarch64 pkgsCross.aarch64-multiplatform.buildGoModule or an aarch64 builder
NixOS test VM Native aarch64-linux builder + KVM
buildGoModule {
  env.CGO_ENABLED = "0";
  env.GOOS = "linux";
  env.GOARCH = "arm64";
}

file the result. Flake packages.x86_64-linux.desk-api-arm64 means built on x86_64, not that the ELF is amd64.

Worked examples

Case 1: linux/arm64 from x86_64, no CGO

Save as go-arm.nix:

# go-arm.nix
{ pkgs ? import <nixpkgs> { } }:

pkgs.buildGoModule {
  pname = "desk-api";
  version = "1.0.0";
  src = ./.;
  vendorHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
  env.CGO_ENABLED = "0";
  env.GOOS = "linux";
  env.GOARCH = "arm64";
}
nix-build go-arm.nix
file result/bin/desk-api

Output (shape):

result/bin/desk-api: ELF 64-bit LSB executable, ARM aarch64, statically linked, …

Do not run it on the x86_64 laptop. Copy to a Pi or use qemu-user once. Static + CGO=0 is why there is no target glibc in the closure.

Case 2: Flake packages per arch

Save as flake.nix:

# flake.nix
{
  description = "desk-api multi-arch";

  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";

  outputs = { self, nixpkgs }:
    let
      pkgs = nixpkgs.legacyPackages.x86_64-linux;
      srcGo = {
        pname = "desk-api";
        version = "1.0.0";
        src = ./.;
        vendorHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
        env.CGO_ENABLED = "0";
        env.GOOS = "linux";
      };
    in
    {
      packages.x86_64-linux = {
        desk-api = pkgs.buildGoModule (srcGo // { env.GOARCH = "amd64"; });
        desk-api-arm64 = pkgs.buildGoModule (srcGo // { env.GOARCH = "arm64"; });
      };
    };
}
nix build .#desk-api-arm64
file result/bin/desk-api

CI: nix build .#desk-api-arm64 on ubuntu-amd64. Seconds, not a QEMU VM.

Case 3: CGO — use pkgsCross or a builder

# go-cgo-arm.nix
{ pkgs ? import <nixpkgs> { } }:

pkgs.pkgsCross.aarch64-multiplatform.buildGoModule {
  pname = "desk-api";
  version = "1.0.0";
  src = ./.;
  vendorHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
  env.CGO_ENABLED = "1";
}

Now you have a target gcc. Slow. Prefer ssh-ng to an ARM machine and native buildGoModule. Mixing GOARCH=arm64 with host gcc is Case 5’s failure mode.

Case 4: Windows / Darwin as GOOS

# go-darwin.nix fragment
{
  env.GOOS = "darwin";
  env.GOARCH = "arm64";
  env.CGO_ENABLED = "0";
}
nix-build
file result/bin/desk-api
Mach-O 64-bit executable arm64

Shipping a Mac binary from Linux CI is possible with CGO off. Notarization is not Nix’s job. GOOS=windows GOARCH=amd64 yields a .exe the same way.

Case 5: file and go version -m

file result/bin/desk-api
go version -m result/bin/desk-api | head
desk-api: go1.24.x
    path    desk/cmd/desk-api
    mod desk    v0.0.0
    build   GOARCH=arm64
    build   GOOS=linux
    build   CGO_ENABLED=0

If linux/amd64 leaked, env.GOARCH did not apply (wrong attr, or CGO forced host).

The trap

The trap is CGO on + GOARCH=arm64 on an x86_64 host without a cross gcc. The build uses the host gcc and produces a confused binary or a compile error. CGO off, or pkgsCross, or a native builder.

The other trap is qemu-user in every GitHub job. Minutes. Native GOARCH with CGO=0 is seconds.

The boring rule

  • CGO off for services you cross.
  • env.GOOS / env.GOARCH on buildGoModule. Probe vendorHash; never leave fakeHash on main.
  • file and go version -m the output.
  • CGO on → pkgsCross or remote native builder.
  • Do not qemu the whole NixOS closure to get a Go binary.

Try this

  1. Case 1; file; confirm aarch64.
  2. Same drv with GOARCH=amd64; file; two different hashes.
  3. Set CGO_ENABLED=1 without a cross gcc; read the error.
  4. go version -m on both binaries and confirm GOARCH.