go2nix Architecture
Technical reference for the go2nix build system.
Overview
go2nix builds Go applications in Nix with two modes that share the same Go CLI and lockfile infrastructure but differ in how they create derivations.
The system has two components:
- A Go CLI (
go2nix) that generates lockfiles, discovers packages and files, compiles packages, and validates lockfile consistency. - A Nix library that reads lockfiles and builds Go applications using one of two modes.
Design context
go2nix builds Go applications at package granularity rather than treating
go build as a single opaque step. The approach is architecturally inspired
by Bazel’s rules_go — both systems work from an explicit package graph —
but go2nix has a much narrower scope: bring package-graph-aware Go builds to
Nix derivations and lockfiles, not replicate a full Bazel rule ecosystem.
For how go2nix compares to buildGoModule, gomod2nix, gobuild.nix, and
nix-gocacheprog, see the
comparison table in the README.
Builder modes
go2nix ships two builders that share the same lockfile and CLI but differ in when the package graph is discovered:
-
Default mode (
buildGoApplication) turns each Go package into its own Nix derivation. go2nix callsgo tool compileandgo tool linkdirectly instead ofgo build, giving Nix full control of the dependency graph at package granularity. The go2nix-nix-plugin (builtins.resolveGoPackages) discovers the package graph at eval time by runninggo listagainst the source tree, so when a dependency changes only the affected packages rebuild. -
Experimental mode (
buildGoApplicationExperimental) provides the same per-package granularity, but discovers the package graph at build time using recursive-nix and content-addressed derivations. Dependency discovery is deferred to the build, so no plugin is required.
See Builder Modes for the full comparison, requirements, and how to choose between them.
Nix directory layout
nix/
├── mk-go-env.nix # Entry point: creates Go toolchain scope
├── scope.nix # Self-referential package set (lib.makeScope)
├── stdlib.nix # Shared: compiled Go standard library
├── helpers.nix # Shared: sanitizeName, escapeModPath, etc.
├── dag/ # Default mode (eval-time DAG)
│ ├── default.nix # buildGoApplication
│ ├── fetch-go-module.nix # FOD fetcher (GOMODCACHE layout)
│ └── hooks/ # Setup hooks (compile, link, env)
└── dynamic/ # Experimental mode (recursive-nix)
└── default.nix # buildGoApplicationExperimental
Entry point: mk-go-env.nix
goEnv = go2nix.lib.mkGoEnv { # == import ./nix/mk-go-env.nix inside this repo
inherit go go2nix;
inherit (pkgs) callPackage;
tags = [ "nethttpomithttp2" ]; # optional
nixPackage = pkgs.nix_234; # optional, enables experimental mode
};
Creates a scope via scope.nix containing both builders plus shared
toolchain.
Package scope: scope.nix
Uses lib.makeScope newScope to create a self-referential package set.
Everything within the scope shares the same Go version, build tags, and
go2nix binary.
Exposes:
buildGoApplication— default mode (eval-time per-package DAG)buildGoApplicationExperimental— experimental mode (recursive-nix)go,go2nix,stdlib,hooks,fetchers,helpers
Shared: stdlib.nix
Compiles the entire Go standard library:
GODEBUG=installgoroot=all GOROOT=. go install -v --trimpath std
Output: $out/<pkg>.a for each stdlib package + $out/importcfg. Shared by
both modes.
Shared: helpers.nix
Pure Nix utility functions:
sanitizeName— Whitelist[a-zA-Z0-9+-._?=],/→-,~→_,@→_at_for derivation names.removePrefix— Substring after a known prefix.escapeModPath— Go module case-escaping (A→!a).
Staleness detection
The lockfile is validated at generation, eval, and build time — see
Lockfile Format → Staleness detection
for the full table. The go2nix check subcommand can also be used standalone
to verify a lockfile without building.