Go Toolchains and gomod2nix
Go Toolchains and gomod2nix
buildGoModule is Cargo’s buildRustPackage. gomod2nix is closer to Crane: it generates Nix from go.mod so module FODs are per-crate, not one giant vendorHash. The boring default is still buildGoModule until a repo’s vendor FOD is painfully slow; then gomod2nix.
Pin the compiler with buildGoModule.override { go = pkgs.go_1_24; } or an overlay. go in nixpkgs 26.05 is the toolchain. dream2nix analogues exist and are not the desk default.
Mental model
| Tool | Like (Rust) | When |
|---|---|---|
buildGoModule + vendorHash |
buildRustPackage + cargoHash |
Default |
gomod2nix |
Crane | Many modules, incremental FODs |
go overlay / .override { go = … } |
fenix / rust-overlay | Pin compiler version |
vendor/ in git |
cargo vendor in-tree |
Air-gap; last resort |
go.mod ──gomod2nix──► gomod2nix.toml + buildGoApplication
──buildGoModule──► one vendorHash FOD
One compiler version per flake. CI, laptop, and buildGoModule share it.
Worked examples
Case 1: Stay on buildGoModule
If nix-build of the API is seconds after a cache hit, stop. gomod2nix is extra files to review.
# desk-api.nix
{ pkgs }:
pkgs.buildGoModule {
pname = "desk-api";
version = "1.0.0";
src = ./.;
vendorHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
}Probe vendorHash. Never leave lib.fakeHash on main.
Case 2: Pin Go like fenix pins rustc
Save as overlay.nix:
# overlay.nix
final: prev: {
deskGo = prev.go_1_24;
buildGoModule = prev.buildGoModule.override { go = final.deskGo; };
}Then pkgs.buildGoModule { … } in that pkgs uses 1.24. Document the version in the flake description. When 26.05’s default go is already what you want, do not overlay.
nix eval -f '<nixpkgs>' go.versionWrite that version down. Bump it with the channel, not with go install.
Case 3: gomod2nix generate
nix run github:nix-community/gomod2nix -- --dir .That writes gomod2nix.toml. Commit it.
Save as flake.nix:
# flake.nix
{
description = "desk-api via gomod2nix (optional)";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
gomod2nix = {
url = "github:nix-community/gomod2nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, gomod2nix }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in
{
packages.${system}.desk-api =
gomod2nix.legacyPackages.${system}.buildGoApplication {
pname = "desk-api";
version = "1.0.0";
src = ./.;
modules = ./gomod2nix.toml;
};
};
}Exact attr names (buildGoApplication vs mk) follow the gomod2nix README for the lock you pin. follows so you do not eval two nixpkgs.
Case 4: vendor/ in git (air-gap)
go mod vendor# vendor.nix fragment
{
src = ./.;
vendorHash = null;
}On current buildGoModule, an in-tree vendor/ plus the right flags skips the FOD. It bloats git. Use when the module proxy is forbidden. Prefer vendorHash FODs on the desk.
Case 5: Private modules
The vendor FOD may use the network. Give Nix access-tokens in nix.conf (CI secrets), not a password in the expression.
# do not
{
GOPROXY = "https://user:hunter2@git.desk.internal";
}# /etc/nix/nix.conf (CI runner / ops)
access-tokens = git.desk.internal=…
GOPRIVATE=git.desk.internal belongs in the devShell, not as a store string that leaks into nix why-depends.
The trap
The trap is switching to gomod2nix because a blog compared it to Crane, then checking in 400 generated files for a 3-module API. Measure nix-build time first.
The other trap is three Go versions: home.packages = [ go_1_22 ], a devShell go_1_24, and buildGoModule default. One overlay / one override. CI and laptop match.
The boring rule
buildGoModulefirst. gomod2nix when vendor FODs hurt.- Pin
gowith.override { go = pkgs.go_1_24; }(or whatever 26.05 offers), not a random tarball. followson gomod2nix’s nixpkgs.- No dream2nix as the desk default.
- One compiler version per flake. Tokens in
nix.conf, not in Nix strings.
Try this
nix eval -f '<nixpkgs>' go.versionon 26.05. Write it down.- Overlay
buildGoModuletogo_1_24if that attr exists;stringsthe binary forgo1.. nix run github:nix-community/gomod2nix -- --help.git grep GOPATHin the flake — should not set a host GOPATH for the build.