Go
The Go module tests, builds, and runs go generate on the Go code in a
workspace. It finds every Go module from its go.mod, and exposes the packages
inside them as a collection keyed by directory. Each package's tests are a
collection keyed by test name, so a check can run one package, one test, or
everything. Each Go module runs on the Go toolchain its own go.mod asks for.
Add it to your workspace
dagger module install dagger.io/go
Checks and generators
| Address | Kind | What it does |
|---|---|---|
go/packages/tests/test | Check | Runs the selected tests, one go test per package |
go/packages/generate | Generator | Runs go generate in the selected packages |
go/packages/generate/stale | Check | Fails when go generate would change a committed file |
dagger check --go # run every Go check
dagger check --go --check=test # run every Go test
dagger generate --go # run go generate in every package
Tests run through an OpenTelemetry-aware runner, so individual Go tests appear as spans in the Dagger TUI and Dagger Cloud.
Collections
| Flag | Collection | Key |
|---|---|---|
--go-package | packages | Package directory, relative to the workspace root, such as sdk/go/cmd/dagger. The root package is . |
--go-test | tests, on a package | Test, fuzz, or example function name, such as TestConnect |
--go-platform | platforms, on a package | GOOS/GOARCH pair, such as linux/arm64 |
List the keys with dagger list go-packages -a, dagger list go-tests -a, and
dagger list go-platforms -a. Combine the flags to narrow a selection:
dagger check --go-package=sdk/go # every check in one package
dagger check --go-test=TestConnect # TestConnect in every package that has one
dagger check --go-package=sdk/go --go-test=TestConnect # one test
dagger generate --go-package=sdk/go/internal/gen # one generator
A test name in two packages is two keys, so --go-test alone selects both and
--go-package picks one. Select collection items describes how to select items.
Packages
A directory is a package key when it has work to do:
- It has tests, and its Go module is selected by the
testsetting. - It is a
mainpackage selected by thebuildsetting. - It has a
//go:generateline and is selected by thegeneratesetting.
Go modules are not a collection of their own. They decide how a package is mounted and which Go toolchain it gets.
Tests
Test names come from the package's _test.go files: every
func TestXxx(t *testing.T), func FuzzXxx(f *testing.F), and
func ExampleXxx(). Listing them runs no container. TestMain, testdata
directories, and nested Go modules are left out. Build constraints are not
evaluated, so a test excluded by one is still listed, and selecting it runs
nothing.
The selected tests of one package run together. With every test in the package
selected, the check runs go test on the package. With some selected, it runs
go test -run with their names. Each package runs as a separate command, never
one go test ./... per Go module.
Platforms
Every pair the Go toolchain supports is a platform key. Platforms select which binaries to cross-compile, and do not affect checks.
Module discovery
The module finds Go modules by looking for go.mod files:
- Every directory at or below the directory
daggerruns from that holds ago.modis a Go module. Rundaggerfrom the workspace root to cover the whole repository, or from a subdirectory to cover only the Go modules below it. - When
daggerruns from a directory inside a Go module, that enclosing module is included as well. - Every
go.modcounts, including those undervendor/,testdata/,examples/, hidden directories, and directories listed in.gitignore. - A directory is a module when its
go.moddeclares a module path and the directory holds at least one Go file of its own.
Exclude a Go module from testing with the test setting, which selects module
roots by path from the workspace root:
| Pattern | Selects |
|---|---|
"**" or "*" | Every module |
"services/api" or "services/api/**" | The module at services/api and any module below it |
"." | The root module only |
"!vendor" | Excludes the module at vendor and any module below it |
Exclusions always win, whatever the order. A list with only exclusions starts
from every module, so ["!vendor", "!testdata"] tests everything except those
two trees, and ["!**"] turns testing off. A module outside the selection
offers no tests.
The generate setting selects directories instead, as Dagger globs from the
workspace root. A literal path selects only that directory, "internal/**"
selects internal and everything below it, and a ! prefix excludes.
The build setting selects main packages by directory relative to each Go
module, with the same rules as test, and applies in every module:
["cmd/**", "!cmd/internal"] keeps cmd and its subdirectories except
cmd/internal.
Generate code
go/packages/generate runs go generate . in each selected package, and
returns the result as a changeset to review before applying.
The selected packages of one Go module run together, in lexical path order, and
share a container, so a later command can use an earlier one's output. With
every package selected, that is go generate ./... for each Go module. A
package generated on its own runs against the committed source of the others.
Go modules run independently.
A directory can pick the container its generators run in with
//go:generate:container, naming a workspace container such as
generate-env, or an image reference such as
docker.io/library/golang:1.26.1-alpine. Without the directive, the directory
uses the module's own base container.
Build binaries
binary is a function, not a check. It returns the binary for a main package
selected by the build setting, built for the engine's platform. On the
platforms collection, it cross-compiles for the selected platforms and
returns one subdirectory per platform, such as linux-amd64/ and
windows-arm64/. With no platform selected, it builds for the engine's
platform. Cross-compiling sets GOOS and GOARCH in the native container, and
cgo is off unless base brings a C cross-toolchain.
dagger -c 'go | packages | get cmd/app | binary | export ./app'
dagger -c 'go | packages | get cmd/app | platforms | subset linux/amd64 darwin/arm64 | batch | binary | export ./dist'
A binary is named the way go build names it.
Configure it
List the current settings with dagger module settings go, then change one with
dagger module settings go <key> <value>. They live in dagger.toml under
[modules.go.settings]:
versionsets one Go toolchain for every module, overriding thegodirective in each module'sgo.mod. Without it, each module is tested, built, and generated ingolang:<version>-alpinefor the version its owngo.modasks for, and a module with nogodirective uses1.26.baseis an optional container to use instead of that image, for tests and generators that need system packages or bound services. It must carry a Go toolchain and any C/C++ dependencies the modules need, and the test runner is installed into it unless it already has one. It supplies its own toolchain, soversionis ignored alongside it and the run reports the ignored value. Wire a function of your own into it with module wiring.includeExtraFiles(default empty) are extra workspace-root path patterns mounted alongside each module's Go source. Go source,go.mod/go.sum/go.work, andtestdata/directories are already included automatically; use this for inputs those patterns miss, such as embedded non-Go assets, generator inputs, or fixtures kept outsidetestdata/.test(default["**"]) selects the module roots to test. See Module discovery for the pattern rules.generate(default["**"]) selects the directories to rungo generatein, as paths and globs from the workspace root.build(default["**"]) selects themainpackages to build, as paths relative to each Go module.buildFlags(default empty) are extra arguments forgo buildonly, such as["-trimpath", "-ldflags=-s -w"]. Each list item is one argument, so a value may contain spaces. Tests andgo generatedo not receive them.goflagssets theGOFLAGSenvironment variable in every Go container, which is how build tags are passed, as in-tags=extended. Flags that take a value use the-flag=valueform. On the command line, put--before the value so the CLI does not read its leading dash as flags of its own.mountPathis the absolute container path where the workspace is mounted. It defaults to/src/<workspace name>, where the name is the last segment of the workspace address, usually the repository name.
# Use one Go version for the whole workspace, whatever each go.mod says
dagger module settings go version 1.26
# Pass build tags to every Go command. The value starts with a dash, so it
# goes after "--"; quoting it is not enough.
dagger module settings go goflags -- -tags=extended
List-valued settings are edited directly in dagger.toml:
[modules.go.settings]
version = "1.26"
includeExtraFiles = ["Makefile", "tools/**"]
test = ["**", "!legacy-service"] # Test all modules except legacy-service and modules below it
build = ["cmd/**"]
buildFlags = ["-trimpath"]
Working with other modules
For Go lint checks, install golangci-lint or Staticcheck alongside this module.
When the tests need a service, such as a database, wire
a container with that service bound into the base setting.