A Nix overlay for Go development. Pure1, reproducible2, and auto-updated3.
- Why it exists?
- Quick Start
- Installation
- Library Functions
- Builder Functions
- Building a Go Application
- Detecting Drift with Git Hooks
- Private Modules
- Using with buildGoModule
- Migration Guides
- Used by
| Feature | go-overlay | gomod2nix | nixpkgs (buildGoModule) |
|---|---|---|---|
| Go versions available | 100+ (1.17 – latest) | nixpkgs only | nixpkgs only |
| New release availability | Up to 4 hours | Days to weeks | Days to weeks |
| Release candidates | Yes | No | No |
| vendorHash required | No | No | Yes |
| Unpatched Go binary | Yes | No | No |
| Go workspaces4 | Yes | No | No |
| Private modules | Standard Go auth | Complex setup | Complex setup |
| Drift detection | Yes (--check) |
No | N/A |
| Circular dependency5 | No | Yes | N/A |
Note
Older Go versions are accessible in nixpkgs by pinning historical commits, but this requires managing multiple nixpkgs inputs and finding the correct commit for each version.
Try Go without installing anything permanently.
Run Go without any setup:
nix run github:purpleclay/go-overlay -- version
# go version go1.25.5 linux/amd64Interactive development:
nix shell github:purpleclay/go-overlay
go version
# go version go1.25.5 linux/amd64Create a derivation:
nix build github:purpleclay/go-overlay
./result/bin/go version
# go version go1.25.5 linux/amd64Pin to a known version using the format go_<major>_<minor>_<patch>:
nix shell github:purpleclay/go-overlay#go_1_23_2
go version
# go version go1.23.2 linux/amd64Add go-overlay to your flake inputs and apply the overlay:
{
description = "My Go Project";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
go-overlay.url = "github:purpleclay/go-overlay";
};
outputs = { nixpkgs, go-overlay, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ go-overlay.overlays.default ];
};
in
{
devShells.default = pkgs.mkShell {
buildInputs = [ pkgs.go-bin.latest ];
};
}
);
}nix develop
go version
# go version go1.25.5 linux/amd64For users not using flakes, go-overlay can be imported directly as an overlay.
Tip
For reproducible builds, pin to a specific commit instead of main:
builtins.fetchTarball "https://github.com/purpleclay/go-overlay/archive/<commit-sha>.tar.gz"Find commit SHAs here.
Direct import in your expression:
let
go-overlay = import (builtins.fetchTarball
"https://github.com/purpleclay/go-overlay/archive/main.tar.gz");
pkgs = import <nixpkgs> {
overlays = [ go-overlay ];
};
in
pkgs.mkShell {
buildInputs = [ pkgs.go-bin.latest ];
}Add to ~/.config/nixpkgs/overlays.nix:
[
(import (builtins.fetchTarball
"https://github.com/purpleclay/go-overlay/archive/main.tar.gz"))
]Then use in any expression:
let
pkgs = import <nixpkgs> {};
in
pkgs.go-bin.latestnix-channel --add https://github.com/purpleclay/go-overlay/archive/main.tar.gz go-overlay
nix-channel --updateThen import the channel:
let
go-overlay = import <go-overlay>;
pkgs = import <nixpkgs> {
overlays = [ go-overlay ];
};
in
pkgs.go-bin.latestGet the absolute latest version, including release candidates.
Use when: You want cutting-edge features and don't mind pre-release software.
go-bin.latestGet the latest stable version, excluding release candidates. Recommended for production.
Use when: You need stability and don't require the latest features.
go-bin.latestStablePin to an exact version for complete reproducibility.
Use when: You need deterministic builds and exact version control.
go-bin.versions."1.21.4"
go-bin.versions."1.25.4"Check if a specific version is available before using it.
Use when: You want to handle missing versions gracefully.
if go-bin.hasVersion "1.22.0"
then go-bin.versions."1.22.0"
else go-bin.latestStableCheck if a version is deprecated (EOL) according to Go's release policy.
Go supports the current and previous minor versions. If the latest stable is 1.25.x:
go-bin.isDeprecated "1.23.4" # true (two versions behind)
go-bin.isDeprecated "1.24.0" # false (previous minor, supported)
go-bin.isDeprecated "1.25.0" # false (current minor, supported)Auto-select Go version from go.mod. Uses toolchain directive if present, otherwise the latest patch of the go directive.
Use when: You want automatic version selection based on your project.
go-bin.fromGoMod ./go.modStrict version matching from go.mod. No automatic patch version selection; fails if exact version is unavailable.
Use when: You need exact reproducibility and want early failure on version mismatch.
go-bin.fromGoModStrict ./go.modNote
fromGoMod is flexible and forgiving—great for development. fromGoModStrict is strict and predictable—better for reproducible builds.
| go.mod Declaration | fromGoMod |
fromGoModStrict |
|---|---|---|
go 1.21 |
Latest 1.21.x | Error |
go 1.21.6 |
1.21.6 | 1.21.6 |
go 1.21 + toolchain go1.21.6 |
1.21.6 | 1.21.6 |
go-overlay provides builder functions for Go applications using vendored dependencies. Unlike nixpkgs' buildGoModule, these work with unpatched Go binaries from go.dev and don't require computing a vendorHash.
Build a Go application using vendored dependencies. Supports two modes:
- In-tree vendor: Use an existing
vendor/directory from your source - Manifest mode: Generate vendor from a
govendor.tomlmanifest
Use when: You want reproducible Go builds without the vendorHash dance.
If your project already has a committed vendor/ directory, simply omit the modules parameter:
buildGoApplication {
pname = "my-app";
version = "1.0.0";
src = ./.;
go = pkgs.go-bin.latest;
subPackages = [ "cmd/my-app" ];
# No modules parameter - uses vendor/ from src
}Use a govendor.toml manifest for dependency management:
buildGoApplication {
pname = "my-app";
version = "1.0.0";
src = ./.;
go = pkgs.go-bin.latest;
modules = ./govendor.toml;
subPackages = [ "cmd/my-app" ];
}| Option | Default | Description |
|---|---|---|
pname |
required | Package name |
version |
required | Package version |
src |
required | Source directory |
go |
required | Go derivation from go-overlay |
modules |
null |
Path to govendor.toml manifest (null = use in-tree vendor) |
subPackages |
["."] |
Packages to build (relative to src) |
ldflags |
[] |
Linker flags |
tags |
[] |
Build tags |
CGO_ENABLED |
inherited from go |
Enable CGO |
GOOS |
inherited from go |
Target operating system |
GOARCH |
inherited from go |
Target architecture |
GOPROXY |
"off" |
Go module proxy URL |
GOPRIVATE |
"" |
Glob patterns for private modules |
GOSUMDB |
"off" |
Checksum database URL |
GONOSUMDB |
"" |
Glob patterns to skip checksum verification |
go-overlay supports local replace directives in go.mod:
replace example.com/mylib => ./libs/mylib
When govendor detects a local replacement, it records the path in govendor.toml:
[mod."example.com/mylib"]
version = "v1.0.0"
hash = "sha256-..."
replaced = "example.com/mylib"
local = "./libs/mylib"During the build, buildGoApplication copies the local module from your source tree into the vendor directory. This works automatically—no additional configuration required.
By default, buildGoApplication sets GOPROXY=off and GOSUMDB=off since dependencies are vendored. However, you can override these for corporate proxies or private module servers:
buildGoApplication {
pname = "myapp";
version = "1.0.0";
src = ./.;
go = pkgs.go-bin.latest;
modules = ./govendor.toml;
# Corporate proxy with fallback
GOPROXY = "https://proxy.corp.example.com,https://proxy.golang.org,direct";
GOPRIVATE = "github.com/myorg/*";
GOSUMDB = "sum.golang.org";
}Build binaries for different platforms by overriding GOOS and GOARCH:
buildGoApplication {
pname = "myapp";
version = "1.0.0";
src = ./.;
go = pkgs.go-bin.latest;
modules = ./govendor.toml;
# Build for Windows on Linux/macOS
GOOS = "windows";
GOARCH = "amd64";
CGO_ENABLED = 0;
}By default, govendor resolves dependencies for these platforms:
linux/amd64,linux/arm64darwin/amd64,darwin/arm64windows/amd64,windows/arm64
To build for platforms outside the defaults (e.g., FreeBSD), use --include-platform when generating the manifest:
govendor --include-platform=freebsd/amd64This ensures dependencies with platform-specific build constraints are included. The additional platforms are persisted in govendor.toml and automatically used on subsequent runs.
For multi-platform releases:
let
platforms = [
{ goos = "linux"; goarch = "amd64"; }
{ goos = "darwin"; goarch = "amd64"; }
{ goos = "freebsd"; goarch = "amd64"; }
];
in
builtins.listToAttrs (map (p: {
name = "myapp-${p.goos}-${p.goarch}";
value = pkgs.buildGoApplication {
pname = "myapp";
version = "1.0.0";
src = ./.;
go = pkgs.go-bin.latest;
modules = ./govendor.toml;
GOOS = p.goos;
GOARCH = p.goarch;
CGO_ENABLED = 0;
};
}) platforms)Build applications from a Go workspace (go.work file). Use this when your project is a monorepo with multiple Go modules that share dependencies. Supports two modes:
- In-tree vendor: Use an existing
vendor/directory fromgo work vendor - Manifest mode: Generate vendor from a
govendor.tomlmanifest
Use when: You have a go.work file coordinating multiple modules in a single repository.
If your workspace already has a committed vendor/ directory (from go work vendor), simply omit the modules parameter:
buildGoWorkspace {
pname = "api";
version = "1.0.0";
src = ./.;
go = pkgs.go-bin.latest;
subPackages = [ "api" ];
# No modules parameter - uses vendor/ from src
}Use a govendor.toml manifest for dependency management:
buildGoWorkspace {
pname = "api";
version = "1.0.0";
src = ./.;
go = pkgs.go-bin.latest;
modules = ./govendor.toml;
subPackages = [ "api" ];
}A typical workspace might look like:
my-monorepo/
├── go.work
├── govendor.toml
├── api/
│ ├── go.mod
│ └── main.go
├── worker/
│ ├── go.mod
│ └── main.go
└── shared/
├── go.mod
└── lib.go
Where go.work contains:
go 1.22
use (
./api
./worker
./shared
)
Run govendor in the workspace root (where go.work lives):
govendorThis generates a govendor.toml that includes:
- External dependencies with NAR hashes
- Workspace modules that are dependencies of other modules
Build each application separately using the same manifest:
# default.nix
{ buildGoWorkspace, go }:
{
api = buildGoWorkspace {
pname = "api";
version = "1.0.0";
src = ./.;
modules = ./govendor.toml;
subPackages = [ "api" ];
inherit go;
};
worker = buildGoWorkspace {
pname = "worker";
version = "1.0.0";
src = ./.;
modules = ./govendor.toml;
subPackages = [ "worker" ];
inherit go;
};
}| Option | Default | Description |
|---|---|---|
pname |
required | Package name |
version |
required | Package version |
src |
required | Source directory (workspace root) |
go |
required | Go derivation from go-overlay |
modules |
null |
Path to govendor.toml manifest (null = use in-tree vendor) |
subPackages |
["."] |
Packages to build (relative to workspace root) |
ldflags |
[] |
Linker flags |
tags |
[] |
Build tags |
CGO_ENABLED |
inherited from go |
Enable CGO |
GOOS |
inherited from go |
Target operating system |
GOARCH |
inherited from go |
Target architecture |
GOPROXY |
"off" |
Go module proxy URL |
GOPRIVATE |
"" |
Glob patterns for private modules |
GOSUMDB |
"off" |
Checksum database URL |
GONOSUMDB |
"" |
Glob patterns to skip checksum verification |
By default, buildGoWorkspace sets GOPROXY=off and GOSUMDB=off since dependencies are vendored. However, you can override these for corporate proxies or private module servers:
buildGoWorkspace {
pname = "api";
version = "1.0.0";
src = ./.;
go = pkgs.go-bin.latest;
modules = ./govendor.toml;
subPackages = [ "api" ];
# Corporate proxy with fallback
GOPROXY = "https://proxy.corp.example.com,https://proxy.golang.org,direct";
GOPRIVATE = "github.com/myorg/*";
GOSUMDB = "sum.golang.org";
}Build binaries for different platforms by overriding GOOS and GOARCH:
buildGoWorkspace {
pname = "api";
version = "1.0.0";
src = ./.;
go = pkgs.go-bin.latest;
modules = ./govendor.toml;
subPackages = [ "api" ];
# Build for Windows on Linux/macOS
GOOS = "windows";
GOARCH = "amd64";
CGO_ENABLED = 0;
}By default, govendor resolves dependencies for these platforms:
linux/amd64,linux/arm64darwin/amd64,darwin/arm64windows/amd64,windows/arm64
To build for platforms outside the defaults (e.g., FreeBSD), use --include-platform when generating the manifest:
govendor --include-platform=freebsd/amd64Create a vendor directory with modules.txt from a parsed govendor.toml manifest. This is a lower-level function used internally by buildGoApplication.
Use when: You need custom control over the vendor directory or build process—for example, when integrating with code generation, custom build steps, or existing stdenv.mkDerivation workflows.
mkVendorEnv {
go = pkgs.go-bin.latest;
manifest = builtins.fromTOML (builtins.readFile ./govendor.toml);
}| Option | Default | Description |
|---|---|---|
go |
required | Go derivation from go-overlay |
manifest |
required | Parsed govendor.toml (via fromTOML) |
src |
null |
Source tree (required if manifest has local modules) |
The resulting derivation contains each module at its import path and a modules.txt with package listings.
{ pkgs }:
let
go = pkgs.go-bin.latest;
vendorEnv = pkgs.mkVendorEnv {
inherit go;
manifest = builtins.fromTOML (builtins.readFile ./govendor.toml);
};
in
pkgs.stdenv.mkDerivation {
pname = "myapp";
version = "1.0.0";
src = ./.;
nativeBuildInputs = [ go ];
configurePhase = ''
export GOCACHE=$TMPDIR/go-cache
export GOPATH=$TMPDIR/go
cp -r ${vendorEnv} vendor
chmod -R u+w vendor
'';
buildPhase = ''
go build -mod=vendor -o myapp ./cmd/myapp
'';
installPhase = ''
mkdir -p $out/bin
cp myapp $out/bin/
'';
}For projects requiring code generation before building:
{ pkgs }:
let
go = pkgs.go-bin.latest;
vendorEnv = pkgs.mkVendorEnv {
inherit go;
manifest = builtins.fromTOML (builtins.readFile ./govendor.toml);
};
in
pkgs.stdenv.mkDerivation {
pname = "myapp";
version = "1.0.0";
src = ./.;
nativeBuildInputs = [ go pkgs.protobuf pkgs.protoc-gen-go ];
configurePhase = ''
export GOCACHE=$TMPDIR/go-cache
export GOPATH=$TMPDIR/go
cp -r ${vendorEnv} vendor
chmod -R u+w vendor
'';
buildPhase = ''
# Generate code first
protoc --go_out=. proto/*.proto
# Then build
go build -mod=vendor -o myapp ./cmd/myapp
'';
installPhase = ''
mkdir -p $out/bin
cp myapp $out/bin/
'';
}Add govendor to your development shell to generate vendor manifests:
# flake.nix
{
inputs.go-overlay.url = "github:purpleclay/go-overlay";
outputs = { self, nixpkgs, go-overlay, ... }:
let
pkgs = import nixpkgs {
system = "x86_64-linux";
overlays = [ go-overlay.overlays.default ];
};
in {
devShells.default = pkgs.mkShell {
buildInputs = [
pkgs.go-bin.fromGoMod ./go.mod
go-overlay.packages.${pkgs.system}.govendor
];
};
};
}Run govendor to generate a govendor.toml manifest:
govendorThis creates a govendor.toml file with NAR hashes for all dependencies. Commit this file to your repository.
Tip
Re-run govendor whenever your dependencies change. Use govendor --check in CI to detect manifest drift, or set up a git hook to catch drift before committing.
Create a default.nix file to build your application:
# default.nix
{
buildGoApplication,
go,
}:
buildGoApplication {
pname = "my-app";
version = "1.0.0";
src = ./.;
inherit go;
subPackages = [ "cmd/my-app" ];
ldflags = [ "-s" "-w" ];
}Add the package to your flake outputs:
# flake.nix
{
packages.default = pkgs.callPackage ./default.nix {
inherit (pkgs) buildGoApplication;
go = pkgs.go-bin.fromGoMod ./go.mod;
};
}Build with:
nix buildUse cachix/git-hooks.nix to automatically check for manifest drift when go.mod or go.work changes:
# flake.nix
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
go-overlay.url = "github:purpleclay/go-overlay";
git-hooks.url = "github:cachix/git-hooks.nix";
};
outputs = { self, nixpkgs, go-overlay, git-hooks, ... }:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
overlays = [ go-overlay.overlays.default ];
};
pre-commit-check = git-hooks.lib.${system}.run {
src = ./.;
hooks = {
govendor = {
enable = true;
name = "govendor";
description = "Check if govendor.toml has drifted from go.mod or go.work";
entry = "${go-overlay.packages.${system}.govendor}/bin/govendor --check";
files = "(^|/)go\\.(mod|work)$";
pass_filenames = true;
};
};
};
in {
devShells.default = pkgs.mkShell {
inherit (pre-commit-check) shellHook;
buildInputs = pre-commit-check.enabledPackages;
};
};
}When you modify go.mod or go.work and attempt to commit, the hook will fail if govendor.toml is out of sync:
govendor.................................................................Failed
- hook id: govendor
- exit code: 1
╭────────┬─────────┬──────────────────────────────────────────────╮
│ File │ Status │ Message │
├────────┼─────────┼──────────────────────────────────────────────┤
│ go.mod │ ✗ drift │ go.mod has changed, regenerate govendor.toml │
╰────────┴─────────┴──────────────────────────────────────────────╯
Run govendor to regenerate the manifest, then commit both files together.
Note
For workspace projects where go.work is gitignored, use --workspace in the hook entry. This traverses up from submodule go.mod files to find the workspace's govendor.toml:
entry = "${go-overlay.packages.${system}.govendor}/bin/govendor --check --workspace";go-overlay supports private Go modules through standard Go authentication mechanisms.
When running govendor, configure authentication via environment variables or .netrc:
# Set GOPRIVATE to bypass the checksum database
export GOPRIVATE="github.com/myorg/*,gitlab.mycompany.com/*"
# Configure git to use token authentication
git config --global url."https://${GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
# Generate manifest
govendorAlternatively, use ~/.netrc:
machine github.com
login oauth2
password ghp_xxxxxxxxxxxx
Configure buildGoApplication with the appropriate environment variables:
buildGoApplication {
pname = "myapp";
version = "1.0.0";
src = ./.;
go = pkgs.go-bin.latest;
GOPRIVATE = "github.com/myorg/*";
GOPROXY = "https://proxy.golang.org,direct";
}For organizations running Athens, Artifactory, or similar:
buildGoApplication {
pname = "myapp";
version = "1.0.0";
src = ./.;
go = pkgs.go-bin.latest;
GOPROXY = "https://athens.mycompany.com";
GOSUMDB = "off";
}buildGoModule defaults to nixpkgs' Go toolchain. To use go-overlay, you must override it.
Warning
Simply passing go as an argument will not work because buildGoModule ignores build arguments for its Go dependency.
# default.nix
{
pkgs,
go,
}:
(pkgs.buildGoModule.override { inherit go; }) {
pname = "my-app";
version = "1.0.0";
src = ./.;
vendorHash = "sha256-...";
}# flake.nix
{
packages.my-app = pkgs.callPackage ./default.nix {
go = pkgs.go-bin.versions."1.22.3";
};
}# flake.nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
gomod2nix.url = "github:nix-community/gomod2nix";
};
outputs = { nixpkgs, gomod2nix, ... }:
let
pkgs = import nixpkgs {
system = "x86_64-linux";
overlays = [ gomod2nix.overlays.default ];
};
in {
packages.default = pkgs.buildGoApplication {
pname = "myapp";
version = "1.0.0";
src = ./.;
modules = ./gomod2nix.toml;
};
};
}gomod2nix generate# flake.nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
go-overlay.url = "github:purpleclay/go-overlay";
};
outputs = { nixpkgs, go-overlay, ... }:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
overlays = [ go-overlay.overlays.default ];
};
in {
packages.default = pkgs.buildGoApplication {
pname = "myapp";
version = "1.0.0";
src = ./.;
go = pkgs.go-bin.fromGoMod ./go.mod;
modules = ./govendor.toml;
};
};
}govendor- Replace gomod2nix with go-overlay in flake inputs
- Update the overlay reference
- Add
goparameter tobuildGoApplication - Run
govendorto generate the new manifest - Delete
gomod2nix.tomland commitgovendor.toml
# flake.nix
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = { nixpkgs, ... }:
let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
in {
packages.default = pkgs.buildGoModule {
pname = "myapp";
version = "1.0.0";
src = ./.;
vendorHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
};
};
}# flake.nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
go-overlay.url = "github:purpleclay/go-overlay";
};
outputs = { nixpkgs, go-overlay, ... }:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
overlays = [ go-overlay.overlays.default ];
};
in {
packages.default = pkgs.buildGoApplication {
pname = "myapp";
version = "1.0.0";
src = ./.;
go = pkgs.go-bin.fromGoMod ./go.mod;
modules = ./govendor.toml;
};
};
}govendor- Add go-overlay to flake inputs
- Add overlay to pkgs
- Replace
buildGoModulewithbuildGoApplication - Remove
vendorHashparameter - Add
goandmodulesparameters - Run
govendorto generate the manifest - Commit
govendor.toml
- devenv - Fast, declarative, reproducible developer environments
Footnotes
-
No side effects—builds depend only on declared inputs, not system state. ↩
-
Given the same inputs, builds produce byte-for-byte identical outputs. Pin a Go version today and get the exact same binary in 5 years. ↩
-
GitHub Actions monitors go.dev every 4 hours. When a new release is detected, a manifest is generated and committed automatically—no manual intervention required. ↩
-
Go workspaces (
go.work) allow multiple modules to be developed together in a monorepo. Neither buildGoModule nor gomod2nix support workspaces because-mod=vendorconflicts with workspace mode. go-overlay'sbuildGoWorkspaceworks around this limitation. ↩ -
gomod2nix's builder depends on its CLI tool, which in turn depends on the builder—complicating NUR integration and bootstrapping. go-overlay avoids this by having
govendorandbuildGoApplicationcommunicate only via the manifest file. ↩