Getting Started
From an empty directory to a built binary, one step at a time, with the default builder. The module, the lockfile, the derivation names and the rebuild counts below are taken from a real build of exactly these files.
What you need
- Nix with flakes enabled, on
x86_64-linux,aarch64-linuxoraarch64-darwin. - The go2nix Nix plugin loaded into the evaluator; step 4 shows how. It builds against Nix 2.34 or newer.
- A
goonPATHfor the two steps that talk to the module proxy: creating the module andgo2nix generate.nix shell nixpkgs#gois enough. The build itself uses the Go from your nixpkgs, not this one.
1. A module
mkdir my-app && cd my-app
go mod init example.com/my-app
// main.go
package main
import "github.com/fatih/color"
func main() {
color.Green("hello from go2nix")
}
go get github.com/fatih/color@v1.18.0
go mod tidy
go.mod now requires four modules, one direct and three indirect:
module example.com/my-app
go 1.26.5
require github.com/fatih/color v1.18.0
require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/sys v0.25.0 // indirect
)
One thing to watch in that file: go mod init wrote the version of the go
you ran into the go line. Keep it at or below the Go of the nixpkgs you
build with (nix eval --raw nixpkgs#go.version). A newer directive makes the
evaluation-time go list try to download that toolchain, and it stops with
go: download go1.99.0 for linux/amd64: toolchain not available. Using
nix shell nixpkgs#go from the same nixpkgs for this step avoids the
question.
2. Pin the modules, or don’t
nix run github:numtide/go2nix -- generate .
(timestamps removed)
level=INFO msg="cache loaded" mods=0
level=INFO msg="collecting modules" dir=.
level=INFO msg="modules found" count=4
level=INFO msg=hashing todo=4 cached=0
level=INFO msg="writing lockfile" mods=4 path=go2nix.toml
# go2nix lockfile v2. Generated by go2nix. Do not edit.
[mod]
"github.com/fatih/color@v1.18.0" = "sha256-pP5y72FSbi4j/BjyVq/XbAOFjzNjMxZt2R/lFFxGWvY="
"github.com/mattn/go-colorable@v0.1.13" = "sha256-qb3Qbo0CELGRIzvw7NVM1g/aayaz4Tguppk9MD2/OI8="
"github.com/mattn/go-isatty@v0.0.20" = "sha256-qhw9hWtU5wnyFyuMbKx+7RB8ckQaFQ8D+8GKPkN3HHQ="
"golang.org/x/sys@v0.25.0" = "sha256-PXZ9EQZ7SFpcL7d3E1+KGTxziYlHEIZPfoXEbnaVD3I="
One hash per module, nothing about packages. Commit it next to go.mod.
go2nix check . compares it with go.mod and says nothing when they agree
(exit status 0).
The lockfile is optional in default mode. With goLock = null the plugin
reads go.sum and computes the same hashes while Nix evaluates, caching them
under ~/.cache/go2nix/nar/. The derivations come out identical either way:
building this module both ways fetched and compiled every dependency once.
What the lockfile adds is a list of hashes that lives in the repository and
is reviewed like any other change, plus a check of go.mod against it at
build time; without one the hashes are recomputed from go.sum and the
module cache. See Lockfile Format.
3. flake.nix
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
go2nix = {
url = "github:numtide/go2nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { nixpkgs, go2nix, ... }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
goEnv = go2nix.lib.mkGoEnv {
inherit (pkgs) go callPackage;
go2nix = go2nix.packages.${system}.go2nix;
};
in
{
packages.${system}.default = goEnv.buildGoApplication {
pname = "my-app";
version = "0.1.0";
src = ./.;
goLock = ./go2nix.toml; # or null
};
};
}
mkGoEnv makes a scope: a Go toolchain, the go2nix CLI the builders call,
and the builder functions that share them. buildGoApplication describes one
application in it. All of its attributes are in the
Builder API.
If the directory is a git repository, git add the files first: a flake only
sees tracked files, and a go.mod Nix cannot see is a go.mod that is not
there.
4. Load the plugin
buildGoApplication calls builtins.resolveGoPackages, which only exists
once the plugin is loaded. For one command:
plugin="$(nix build --no-link --print-out-paths \
github:numtide/go2nix#go2nix-nix-plugin)/lib/nix/plugins/libgo2nix_plugin.so"
nix build --option plugin-files "$plugin"
Permanently: add plugin-files = <that path> to nix.conf, or on NixOS set
nix.settings.plugin-files from
inputs.go2nix.packages.${pkgs.system}.go2nix-nix-plugin.
To see whether an evaluator has it:
nix eval --option plugin-files "$plugin" --expr 'builtins ? resolveGoPackages'
# true
Without it, evaluation stops at attribute 'resolveGoPackages' missing. The
plugin has to be built against the Nix that loads it; see
Nix Plugin.
5. Build
nix build --option plugin-files "$plugin"
./result/bin/my-app
# hello from go2nix
result/bin/ holds one binary per main package, named after its directory,
or after pname for the package at the module root. What Nix built to get
there, by derivation name:
| Derivation | What it is |
|---|---|
go-stdlib-… | the standard library, compiled once per toolchain and goEnv |
gomod-github.com-fatih-color-v1.18.0 and three more | one fetch per module |
gopkg-github.com-fatih-color-v1.18.0, gopkg-github.com-mattn-go-colorable-v0.1.13, gopkg-github.com-mattn-go-isatty-v0.0.20, gopkg-golang.org-x-sys-unix-v0.25.0 | one compile per third-party package that main.go reaches: x/sys is one package here, not the module |
golocal-example.com-my-app | one compile per package of yours |
my-app-deps-importcfg, my-app-test-deps-importcfg | the import maps the link and the tests read |
my-app-0.1.0 | compiles main, links, runs the tests |
Incremental Builds explains what each one is keyed on.
6. Change something
Edit main.go and build again. Two derivations run:
golocal-example.com-my-app and my-app-0.1.0. Nothing is fetched and no
dependency is recompiled.
Import another package from a module you already require. Nothing to regenerate: the lockfile pins modules, and the package graph is rediscovered on every evaluation.
Add or bump a module.
go get github.com/spf13/cobra@latest
go mod tidy
nix run github:numtide/go2nix -- generate .
generate reuses the hashes already in go2nix.toml and only downloads what
is new. If you forget, evaluation fails with
error: attribute '"github.com/spf13/cobra@<version>"' missing: the package
graph uses a module the lockfile does not have. (With goLock = null there is
nothing to forget.)
Where next
- Recipes: a module inside a larger repository, private modules, cgo, static binaries.
- Builder API: every attribute, tests, cross-compilation, private modules.
- Test Support: what
doCheckruns. - Package Overrides: cgo packages that need system libraries.
- Builder Modes: the default builder against the experimental one.
- Troubleshooting: the errors above and the rest.