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

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:

ImportTarget
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-libxnot 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:

  1. Set the dependency's import_path to github.com/original/pkg (for correct imports)
  2. Set the fetch_path to github.com/myfork/pkg (where to actually fetch from)
  3. 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:

  1. Fetches the source from fetch_path (the fork)
  2. Stores it in the vendor directory under import_path (the original path)
  3. 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 replaceResult
=> github.com/fork v1.2.3Uses 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