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 three components:
- A Go CLI (
go2nix) that generates and validates lockfiles and is what the derivations run: it compiles one package, links a binary, and builds and runs the tests. - A Nix library (
nix/) that turns the package graph into derivations, in one of two modes. - A Nix plugin (
packages/go2nix-nix-plugin/, a Rust core and a C++ shim) that gives the default mode its package graph at eval time. It is built separately and has to be loaded into the evaluator.
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 (one module's extracted source tree)
│ └── 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 (pkgs) go callPackage;
go2nix = go2nix.packages.${system}.go2nix; # the CLI, not the flake
goEnv = { CGO_ENABLED = "0"; }; # optional, env for stdlib and every go tool call
netrcFile = null; # optional, for private modules
nixPackage = pkgs.nixVersions.nix_2_34; # 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 toolchain, goEnv, standard
library and go2nix binary. (mkGoEnv also accepts tags and stores it on the
scope, but neither builder reads it: build tags are the per-call tags
argument.)
Exposes:
buildGoApplication— default mode (eval-time per-package DAG)buildGoApplicationExperimental— experimental mode (recursive-nix)go,go2nix,stdlib,hooks,fetchers(fetchGoModule),helpers, andgoEnv(the env attrset, withGOOS/GOARCHdefaulted in when cross-compiling)
Shared: stdlib.nix
Compiles the entire Go standard library:
GODEBUG=installgoroot=all GOROOT="$NIX_BUILD_TOP" go install -v --trimpath std
after copying the toolchain’s src, pkg and lib there. Output:
$out/<pkg>.a for each stdlib package + $out/importcfg. There is one such
derivation per toolchain and scope goEnv (the variables are exported before
the build and a hash of them is part of the name), shared by every build in
the scope and by both modes.
Shared: helpers.nix
Pure Nix utility functions:
sanitizeName—/→-,~→_,@→_at_for derivation names, and anything longer than 160 characters is cut and given an 8-hex-digit hash suffix. The Go and Rust counterparts (pkg/nixdrv/sanitize.go,resolve.rs) additionally replace characters outside[a-zA-Z0-9+-._?=]; the three agree on valid import paths and must be kept in sync.removePrefix— Substring after a known prefix.escapeModPath— Go module case-escaping (A→!a).normalizeSubPackages— adds the missing./tosubPackagesentries.goModLocalReplaceDirs,parseLocalReplaces— read the filesystemreplacetargets out of ago.mod, for callers that build their own source filter.
Staleness detection
A lockfile is checked when it is generated and again at build time, by
link-binary; a module the graph needs and the lockfile lacks already fails
evaluation — see
Lockfile Format → Staleness detection
for the full table. The go2nix check subcommand can also be used standalone
to verify a lockfile without building.