Go Modules with buildGoModule
Go Modules with buildGoModule
Go is how the desk API ships. The boring default is: pkgs.buildGoModule with a real vendorHash, committed go.mod / go.sum, CGO_ENABLED=0 unless you need C, and the Go from nixpkgs 26.05 — not go get in the sandbox.
This is the Go analogue of buildRustPackage: one FOD for modules, one offline compile.
Mental model
go.mod + go.sum
│ vendorHash (FOD)
▼
/nix/store/…-desk-api-go-modules
│ go build -mod=vendor (offline)
▼
$out/bin/desk-api
| Field | Role |
|---|---|
vendorHash |
Hash of the vendored module tree. Probe with lib.fakeHash. |
vendorHash = null |
Only when there are zero module deps (stdlib only). |
proxyVendor |
Let the FOD use the module proxy (usual). |
modRoot |
If go.mod is not at src root. |
subPackages |
["cmd/desk-api"] so you do not build every ./... main. |
env.CGO_ENABLED |
"0" for static-ish binaries; "1" needs gcc in nativeBuildInputs. |
go test runs in checkPhase when doCheck = true (the default for buildGoModule). Tests must be hermetic.
Worked examples
Case 1: Stdlib-only binary
Save as desk_service_go.nix:
# desk_service_go.nix
{ lib, buildGoModule }:
buildGoModule {
pname = "desk-service";
version = "1.0.0";
src = ./.;
vendorHash = null;
meta = {
description = "Desk Go backend";
license = lib.licenses.mit;
};
}With go.mod + main.go in . (no require lines):
nix-build -E 'with import <nixpkgs> {}; callPackage ./desk_service_go.nix {}'
./result/bin/desk-serviceOutput:
Desk Go backend operational
(Your fmt.Println text.)
Case 2: Dependencies — probe vendorHash
# desk_api.nix
{ buildGoModule, lib }:
buildGoModule {
pname = "desk-api";
version = "1.0.0";
src = lib.fileset.toSource {
root = ./.;
fileset = lib.fileset.unions [ ./go.mod ./go.sum ./main.go ./internal ];
};
vendorHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
subPackages = [ "." ];
}nix-buildMismatch prints got: sha256-…. Paste it. Changing go.mod without bumping the hash is a stop, not a network call in buildPhase.
Case 3: Pin the toolchain
# go-pin.nix
{ pkgs ? import <nixpkgs> { } }:
(pkgs.buildGoModule.override { go = pkgs.go_1_24; }) {
pname = "desk-api";
version = "1.0.0";
src = ./.;
vendorHash = null;
}pkgs.go on 26.05 is whatever that channel ships. When the desk must match Boring-Go’s 1.27 (or a security pin), override the builder, not PATH in a wrapper. Confirm with:
nix-build
./result/bin/desk-api
# or: strings result/bin/desk-api | grep '^go1\.'Case 4: CGO_ENABLED=0
buildGoModule {
pname = "desk-api";
version = "1.0.0";
src = ./.;
vendorHash = null;
env.CGO_ENABLED = "0";
ldflags = [ "-s" "-w" ];
}No gcc. Cross to GOOS=linux is then just the Go toolchain. If you import net with cgo DNS, you want this anyway for boring deploys. SQLite with mattn/go-sqlite3 needs CGO on and nativeBuildInputs = [ pkgs.pkg-config ]; plus the C library — then you left the boring path.
Case 5: Tests in the sandbox
buildGoModule {
pname = "desk-api";
version = "1.0.0";
src = ./.;
vendorHash = "sha256-…";
doCheck = true;
# default checkPhase is go test
}A test that dials https://api.github.com fails. Fixtures in testdata/. vendorHash must include test-only modules too (go.sum lines).
# go-api.nix fragment
{
env.GOTOOLCHAIN = "local";
env.CGO_ENABLED = "0";
ldflags = [
"-s" "-w"
"-X main.version=${version}"
];
tags = [ "netgo" ];
proxyVendor = true; # FOD may use proxy.golang.org; buildPhase stays offline
}GOTOOLCHAIN=local stops Go 1.21+ from downloading a newer toolchain because go.mod says go 1.25. The Nix go is the compiler. proxyVendor = true is the usual FOD; false expects an in-tree vendor/. Do not set both vendorHash = null and a populated go.sum.
The trap
The trap is vendorHash = null with a require in go.mod. Eval may succeed; the build tries the network and dies. null is not “figure it out.”
The other trap is go get / GOPROXY=https://proxy.golang.org in buildPhase. The sandbox has no network. The FOD already fetched.
The boring rule
buildGoModule+vendorHash(ornullonly for stdlib-only).- Commit
go.modandgo.sum. Filtersrcto them + packages. subPackagesfor the mains you ship.CGO_ENABLED=0unless a C library is the point.GOTOOLCHAIN=localon the derivation.- Pin
goviabuildGoModule.override { go = …; }when the channel default is not the desk compiler. ldflags/subPackages/tagson the builder, not abuildPhasethat callsgo buildby hand.
Try this
- Stdlib
main.go;vendorHash = null;nix-build; run the binary. go get github.com/google/uuidin a throwaway module; setfakeHash; pastegot.- Flip one character of
vendorHash; read the mismatch. env.CGO_ENABLED = "0";file result/bin/desk-api— dynamically linked to musl/glibc still possible via Go’s net; the point is no gcc.