Monorepo Git Patterns with go work
One Git repository can host multiple Go modules coordinated by go.work during development.
Search across all documentation pages
One Git repository can host multiple Go modules coordinated by go.work during development.
Git history stays unified while each module keeps its own go.mod, tags, and consumer graph.
Monorepos suit platform teams shipping shared libraries and services together.
Use go.work locally to replace cross-module replace directives in committed go.mod files.
Tag and CI each module explicitly so proxies and consumers resolve the right versions.
Quick-reference recipe card - copy-paste ready.
# Repo layout
# services/api/go.mod
# libs/auth/go.mod
cd repo-root
go work init ./services/api ./libs/auth
go work use -r . # optional: discover modules
# Develop across modules
cd services/api
go test ./...
# Validate publish view (no workspace)
GOWORK=off go test ./...When to reach for this:
replace paths on shared branches.go.mod replace lines.acme-platform/
go.work
go.work.sum
libs/telemetry/go.mod # github.com/acme/telemetry
services/billing/go.mod # github.com/acme/billing
// go.work
go 1.26
use (
./libs/telemetry
./services/billing
)# Change telemetry API and fix billing in one branch
git switch -c feat/telemetry-batch
# edit libs/telemetry/...
# edit services/billing/...
go test ./...
cd services/billing && GOWORK=off go test ./...
git add libs/telemetry services/billing go.work go.work.sum
git commit -m "telemetry: add batch export; billing: adopt batch API"
git push -u origin feat/telemetry-batchTag only the library module when releasing for external consumers:
git switch main && git pull --ff-only
cd libs/telemetry
git tag -a v0.4.0 -m "telemetry v0.4.0"
git push origin v0.4.0What this demonstrates:
go.work wires local replaces implicitly during dev.GOWORK=off simulates what module consumers experience.go.mod at a subdirectory root.go.work at repo root lists module directories in use directives.go.mod files in one PR.go.work.sum records checksums for workspace resolution, similar to go.sum.
Commit go.work when the whole team shares the workspace layout.
Keep personal experiments in untracked go.work or GOWORK env override.
| Pattern | Purpose |
|---|---|
| Path-scoped PR labels | Show which modules a PR affects |
| Per-module CODEOWNERS | Route review to owning team |
Tag prefix telemetry/v0.4.0 | Disambiguate multi-module tags (document in README) |
CI matrix per go.mod | Faster feedback on large repos |
GOWORK=off release job | Verify publishable module graph |
| Mechanism | Committed to shared main? | Consumer impact |
|---|---|---|
go.work | Optional team-wide dev aid | None (not published) |
replace in go.mod | Avoid for local paths | Breaks go get for external users |
| Versioned require | Yes | Normal consumer path |
strategy:
matrix:
module: [libs/telemetry, services/billing]
steps:
- run: cd ${{ matrix.module }} && GOWORK=off go test ./...Root workspace job can still run go test ./... from go.work for integration confidence.
replace ../libs paths - Break external clones. Fix: go.work for local dev; require semver versions on main.internal/ spans modules - Compiler enforces per module root. Fix: shared code in published packages or single module until split is real.GOWORK=off in release CI - Ships workspace-only graph surprises. Fix: mandatory off mode before tag.| Alternative | Use When | Don't Use When |
|---|---|---|
| Single module | One release line, simple graph | Independent semver per subsystem |
| Multi-repo | Hard team boundaries | Frequent atomic cross-cutting changes |
| Bazel / custom build | Non-Go polyglot orchestration | Standard go command workflows suffice |
| Git submodules | Legacy separation | Go module ergonomics preferred |
Yes when all developers use the same module set daily.
No when it is a personal superset of modules; use local untracked file instead.
By module path in each go.mod, not repo root.
go get github.com/acme/telemetry@v0.4.0 fetches only that module subtree.
Yes.
Run tests with workspace on and GOWORK=off per affected module before merge.
Separate module paths (/v2) and tags per major.
Workspace can include both lines during migration.
No.
It overrides resolution locally; go.mod still lists intended semver requires for consumers.
Extract history with filter-repo or copy subtree, publish new module path, retract old paths if needed.
Plan import path migration in changelog.
Possible in each module root.
Refresh vendor in the PR that bumps that module's deps only.
Same trunk patterns as single-module repos.
Add module name in branch (feat/telemetry-batch) for clarity.
go.work supports replace blocks for local overrides.
Prefer use paths over fragile absolute replaces.
Use path filters and git diff --name-only to select matrix entries.
Still run periodic full workspace integration on main.
Rarely worth it.
Go modules already version dependencies; submodules add VCS friction.
State which module each tag versions, default branch, and GOWORK=off check commands for maintainers.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 18, 2026