Go Support
Turnkey provides comprehensive Go support with Buck2 integration.
Setup
Add to toolchain.toml:
[toolchains]
go = {}
godeps-gen = {}
Enable Go dependencies in flake.nix:
turnkey.toolchains.buck2.go = {
enable = true;
depsFile = ./go-deps.toml;
};
Project Structure
my-project/
├── go.mod
├── go.sum
├── go-deps.toml # Generated from go.mod
├── cmd/
│ └── myapp/
│ ├── main.go
│ └── rules.star
└── pkg/
└── mylib/
├── lib.go
└── rules.star
Build Rules
In rules.star:
load("@prelude//go:go.bzl", "go_binary", "go_library")
go_binary(
name = "myapp",
srcs = ["main.go"],
deps = ["//pkg/mylib:mylib"],
)
Tests
A go_test compiles its srcs with the package of its target_under_test,
as go test does:
go_test(
name = "mylib_test",
srcs = glob(["*_test.go"]),
target_under_test = ":mylib",
)
Test files can be in the package itself (package mylib) or in an external
test package (package mylib_test), which sees only the package's exported
API. Both kinds can be in the same go_test.
External Dependencies
Reference third-party packages via the godeps cell:
go_library(
name = "mylib",
srcs = ["lib.go"],
deps = ["godeps//vendor/github.com/pkg/errors:errors"],
)
Auto-Sync
The go command is wrapped to auto-sync dependencies:
go get github.com/some/package # Triggers sync
go mod tidy # Triggers sync
Multiple Modules: go.work
A repo with several Go modules declares them as a go.work workspace at the project root. This is the only supported multi-module shape (ADR 0007). Go resolves all the members together, so each third-party module has one version across the repo and one godeps cell. A repo without a go.work is a workspace of one member: the root go.mod.
my-monorepo/
├── go.work # use ./app ./shared-lib ./third_party/cobra-fork
├── go-deps.toml # Generated by tk sync, lists the members
├── app/
│ ├── go.mod # module github.com/company/app
│ └── cmd/server/
│ ├── main.go # imports github.com/company/shared-lib/log
│ └── rules.star
├── shared-lib/
│ ├── go.mod # module github.com/company/shared-lib
│ └── log/
│ ├── log.go
│ └── rules.star
└── third_party/cobra-fork/ # a patched local fork of github.com/spf13/cobra
├── go.mod # module github.com/spf13/cobra
└── rules.star
How imports map
The members are the only first-party Go code. tk sync records them in go-deps.toml under [members], and rules sync maps each import to the member whose module path is its longest prefix on a / boundary:
| Import | Target |
|---|---|
github.com/company/shared-lib/log | //shared-lib/log:log |
github.com/spf13/cobra | //third_party/cobra-fork:cobra |
github.com/spf13/cobra/doc | //third_party/cobra-fork/doc:doc |
github.com/company/shared-libx | not a member: godeps//vendor/... |
A package's target is named after its import path's last component, as in the godeps cell. That matters at a member's root: the root package of github.com/spf13/cobra in third_party/cobra-fork is :cobra, not :cobra-fork after its directory.
Local forks and replace
A member can be a local fork of a third-party module: give it the upstream module path and add it to go.work. Go then builds every import of that module from the fork, including imports from other third-party modules. In the godeps cell, those imports reach the member's targets through forwarding alias packages (ADR 0008).
A local-path replace directive, in go.work or a member's go.mod, must point at a workspace member. tk sync fails on one that points anywhere else: add the directory to go.work.
Modules outside the workspace
A go.mod that isn't a member is outside the Go build, as it is for go build ./... in the workspace. Rules sync leaves the Go rules under it alone. Test fixtures with their own go.mod need no fencing.
External Fork Replacements
Turnkey supports replace directives that point to external forks (not local paths). This is useful when:
- Using a forked version of a dependency with bug fixes
- Using a maintained fork of an abandoned project
- Testing changes before upstreaming
How It Works
When godeps-gen encounters an external replace directive like:
replace github.com/original/pkg => github.com/myfork/pkg v1.2.3
It will:
- Set the dependency's
import_pathtogithub.com/original/pkg(for correct imports) - Set the
fetch_pathtogithub.com/myfork/pkg(where to actually fetch from) - Use the replacement version
In go.mod
module github.com/company/myapp
require github.com/original/pkg v1.0.0
replace github.com/original/pkg => github.com/myfork/pkg v1.2.3
Generated go-deps.toml
[deps."github.com/original/pkg@v1.2.3"]
import_path = "github.com/original/pkg"
fetch_path = "github.com/myfork/pkg"
version = "v1.2.3"
hash = "sha256-..."
How the Cell Builder Uses This
The Nix cell builder:
- Fetches the source from
fetch_path(the fork) - Stores it in the vendor directory under
import_path(the original path) - Generates Buck2 rules using the original import path
This means your code continues to import from the original path (github.com/original/pkg), but the actual source comes from your fork.
Version Handling
External replaces can change the version:
| go.mod replace | Result |
|---|---|
=> github.com/fork v1.2.3 | Uses v1.2.3 from fork |
=> github.com/fork (no version) | Uses the required version from fork |
Version-specific replaces are also supported:
// Only replace v1.0.0, not other versions
replace github.com/pkg v1.0.0 => github.com/fork/pkg v1.0.1
Common Use Cases
Using a fork with a fix:
// Your fork has a critical bug fix not yet merged upstream
replace github.com/upstream/logger => github.com/you/logger v1.0.1-patched
Using a maintained fork:
// Original project abandoned, using community fork
replace github.com/old/abandoned => github.com/community/maintained v2.0.0
Testing before upstreaming:
// Test your changes before creating a PR
replace github.com/original/pkg => github.com/you/pkg v0.0.0-20240101