Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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-linux or aarch64-darwin.
  • The go2nix Nix plugin loaded into the evaluator; step 4 shows how. It builds against Nix 2.34 or newer.
  • A go on PATH for the two steps that talk to the module proxy: creating the module and go2nix generate. nix shell nixpkgs#go is 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:

DerivationWhat it is
go-stdlib-…the standard library, compiled once per toolchain and goEnv
gomod-github.com-fatih-color-v1.18.0 and three moreone 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.0one compile per third-party package that main.go reaches: x/sys is one package here, not the module
golocal-example.com-my-appone compile per package of yours
my-app-deps-importcfg, my-app-test-deps-importcfgthe import maps the link and the tests read
my-app-0.1.0compiles 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