Go (Experimental)
Use experimental native Go workspace support with Turborepo.
Turborepo can discover modules in a repository-root go.work as packages, add their module dependencies to the Package Graph, and map common tasks to standard Go commands.
Native Go workspace support is experimental and can change at any time. The initial supported range is Go 1.22 and newer. Please provide feedback on the Go RFC.
Enable Go workspaces
Set the Future Flag in the root turbo.json:
{
"$schema": "https://turborepo.dev/schema.json",
"futureFlags": {
"experimentalGoWorkspaces": true
},
"tasks": {}
}Prerequisites
The repository root must contain:
- Go 1.22 or newer available as
goonPATH - A
go.workwhoseusedirectives identify at least one module inside the repository - A
go.modwith a module path in every member - Current
go.sumandgo.work.sumfiles when the resolved graph requires them
go 1.22
use (
./apps/api
./packages/lib
)Package names
Turborepo derives each Go package name from the last component of its go.mod module path, preserving a trailing major-version suffix:
| Go module path | Turborepo package name |
|---|---|
example.com/api | api |
example.com/acme/api | api |
example.com/api/v2 | api/v2 |
Use the derived name in filters, task identifiers, and prune targets. For a module declared as module example.com/api:
turbo ls --filter=api
turbo run test --filter=api
turbo run api#testFor module example.com/api/v2, use --filter=api/v2 or api/v2#test. The suffix is part of the package identity, so api and api/v2 are distinct packages. Go uses /vN major-version suffixes starting at v2; v0 and v1 normally use the unversioned path. The gopkg.in spelling is preserved too: gopkg.in/yaml.v3 becomes yaml.v3.
Directory filters are unchanged: --filter=./apps/api selects by filesystem location, while --filter=api selects by name. A slash within a name, such as api/v2, does not make it a directory filter.
Keep full module paths in go.mod declarations, require and replace directives, and Go imports. Turborepo retains those paths as dependency-resolution metadata; the derived package name does not change how Go resolves modules. Full module paths are not aliases for Turborepo package names, and package-name overrides are not supported.
Package identities must be unique across the entire Package Graph, including packages from other languages. For example, example.com/api and example.org/api both derive api and cannot coexist as workspace members. A Go module named api also collides with a JavaScript package whose package.json name is api. Resolve collisions by changing the source package name or Go module path so that the derived names differ; changing only the module's domain or parent path does not help.
The synthetic go-workspace scope depends on every member. That identity is reserved: a module such as example.com/go-workspace cannot derive the name go-workspace.
Built-in tasks
| Scope | Turborepo task | Go command |
|---|---|---|
Module with zero or multiple main packages | turbo run build | go build ./... |
Module with exactly one main package | turbo run build | go build <target> |
Module with exactly one main package | turbo run dev | go run <target> |
| Any module | turbo run test | go test ./... |
| Any module | turbo run lint | go vet ./... |
| Any module | turbo run format | go fmt ./... |
Running turbo run test, turbo run lint, or turbo run format from the repository root runs the corresponding command in each member module. Filtered runs select the same per-module tasks, so local filtered test and lint runs share cache entries with unfiltered CI runs. There are no built-in go-workspace#test, go-workspace#lint, or go-workspace#format tasks; omit --filter=go-workspace to run verification across all modules.
Formatting stays uncached and package-aware, leaving testdata and nested non-member modules untouched. go fmt does not support cross-module workspace patterns.
For a module with exactly one runnable main package, Turborepo selects that package as <target> (. or a relative path such as ./cmd/server) and runs the command from the module directory. Builds use Go's default output behavior: the binary is written directly into the module directory, not dist/ or bin/. Its name is the last non-major-version component of the package import path, with .exe appended for Windows. For example, go build ./cmd/server produces ./server, and a main package at the root of module example.com/api/v2 produces ./api. This binary filename is separate from the Turborepo package name, which remains api/v2.
Go also reports its normal output-name conflicts. For example, a main package in ./server cannot produce ./server from the module directory because that source directory already exists. Use an explicit -o command override for these layouts.
Turborepo does not guess when a module has zero or multiple runnable main packages. Those modules do not receive dev; their build runs go build ./... as a compilation check without producing deployable binaries. This decision is per module, not per go.work: multiple modules with one main package each can each produce a binary.
Arguments after Turborepo's -- are placed before package patterns for build, test, lint, and format. Arguments to dev follow the main import path and become program arguments.
turbo run test --filter=api -- -run TestHTTP
turbo run dev --filter=api -- --port 8080Override or remove defaults
Normal task configuration applies to built-in tasks. Package Configuration can remove a default:
{
"extends": ["//"],
"tasks": {
"format": {
"extends": false
}
}
}Enable experimentalTaskCommand to replace a command without a shell:
{
"futureFlags": {
"experimentalGoWorkspaces": true,
"experimentalTaskCommand": true
},
"tasks": {
"test": {
"command": {
"go": ["go", "test", "-race", "./..."]
}
}
}
}To preserve a custom binary path, enable experimentalTaskCommand in the root configuration as above, then replace the build command in the module's Package Configuration:
{
"extends": ["//"],
"tasks": {
"build": {
"command": ["go", "build", "-o", "bin/my-api", "."],
"inputs": ["$TURBO_DEFAULT$", "!bin/my-api"],
"outputs": ["bin/my-api"]
}
}
}command replaces the entire build command. outputs declares files to cache; it does not change Go's output path. Exclude custom generated binaries from inputs so they do not invalidate their own build cache.
Hashing and caching
Native task hashes include:
- Module sources and transitive internal module sources
go.mod,go.sum,go.work, andgo.work.sum- Each module's transitive external module identities, versions, replacements, and checksums
go versionand stable target/compiler fields fromgo env -json- Behavior-changing variables such as
GOOS,GOARCH,GOFLAGS,GOTOOLCHAIN,CGO_ENABLED, and C toolchain flags
Checkout paths, GOCACHE, GOMODCACHE, credentials, proxy authentication, and telemetry settings are excluded. Go's mutable build and module caches remain Go's responsibility and are never Turborepo outputs.
A module with one runnable main package gets one restorable <name> binary in the module directory (<name>.exe for Windows). Turborepo excludes generated binary paths for both Windows and non-Windows targets from automatic module and dependency source inputs, while preserving tracked files, Go source files, embedded inputs, and same-named directories. Automatic build caching is disabled when the inferred output may overlap source inputs or source classification is unavailable. Library and multi-main builds default to uncached because Go's internal build artifacts have no stable output for Turborepo to restore. dev and source-mutating format tasks are uncached. Build arguments disable automatic output caching because flags can relocate or reshape the binary.
Affectedness and watch mode
Source changes affect their owning module and internal dependents. go.mod and go.work changes trigger repository rediscovery; go.sum and go.work.sum changes invalidate resolution. In-repository Go build and module caches are ignored by the watcher.
--affected, dependency filters, ^task ordering, turbo query, and turbo watch consume the same module relationships.
Prune
turbo prune apiThe output keeps the selected module and required internal dependencies, rewrites go.work with deterministic explicit members, preserves go, toolchain, and applicable replacement directives, and copies go.work.sum plus each retained module directory.
Validate a pruned output with ordinary Go commands:
cd out/apps/api
go test ./...