Go Workspace Mode for Multi-Module Development
Use go.work files to develop multiple Go modules together without replace directives, simplifying local multi-module workflows.
What you'll learn
- ✓What Go workspace mode solves and when to use it
- ✓Creating and managing go.work files
- ✓Developing multiple modules together without replace directives
- ✓Integrating workspaces with CI and version control
Prerequisites
None — this post is self-contained.
Before Go 1.18, developing two modules together locally required replace directives in go.mod. You would point one module’s dependency at a local path, do your work, then remember to remove the directive before committing. Workspace mode eliminates this friction. A go.work file sits above your modules and tells the Go toolchain to use local versions automatically.
The Problem With Replace Directives
Suppose you have a shared library and a service that imports it:
projects/
shared-lib/
go.mod (module github.com/yourorg/shared-lib)
api-service/
go.mod (module github.com/yourorg/api-service)
To develop them together, you would add a replace directive in api-service/go.mod:
replace github.com/yourorg/shared-lib => ../shared-lib
This works but creates problems. The directive must be removed before pushing. CI builds fail if the local path does not exist. Multiple developers must all have the same directory layout. It does not scale to three or more modules.
Creating a Workspace
Initialize a workspace from a parent directory:
cd projects
go work init ./shared-lib ./api-service
This creates a go.work file:
go 1.22
use (
./shared-lib
./api-service
)
Now any go command run inside this directory tree uses local versions of modules listed in use. No replace directives needed.
How It Works
When the Go toolchain detects a go.work file in the current directory or any parent directory, it enters workspace mode. In this mode:
- All modules listed in
useare resolved locally. - If
api-serviceimportsgithub.com/yourorg/shared-lib, the toolchain uses the local copy from./shared-libinstead of downloading it. - Each module’s own
go.modremains unchanged and valid for independent builds.
The workspace is a development-time overlay. It does not affect how the modules behave when built outside the workspace.
Adding and Removing Modules
Add a new module to an existing workspace:
go work use ./new-module
Remove a module:
go work edit -dropuse ./old-module
You can also list modules from subdirectories:
go work use ./services/auth ./services/billing ./pkg/common
Workspace Structure for a Monorepo
A typical monorepo layout with workspaces:
monorepo/
go.work
pkg/
database/
go.mod
db.go
auth/
go.mod
auth.go
services/
api/
go.mod
main.go
worker/
go.mod
main.go
The go.work file:
go 1.22
use (
./pkg/database
./pkg/auth
./services/api
./services/worker
)
Changes to pkg/database are immediately visible when building services/api, with no publishing step and no replace directives.
A Practical Example
Create a shared types package and a service that uses it:
// pkg/database/go.mod
module github.com/yourorg/monorepo/pkg/database
go 1.22
// pkg/database/db.go
package database
import (
"context"
"fmt"
)
type User struct {
ID string
Email string
Name string
}
type Store interface {
GetUser(ctx context.Context, id string) (*User, error)
}
type MemoryStore struct {
users map[string]*User
}
func NewMemoryStore() *MemoryStore {
return &MemoryStore{users: make(map[string]*User)}
}
func (s *MemoryStore) GetUser(ctx context.Context, id string) (*User, error) {
u, ok := s.users[id]
if !ok {
return nil, fmt.Errorf("user %s not found", id)
}
return u, nil
}
The API service imports the database package:
// services/api/go.mod
module github.com/yourorg/monorepo/services/api
go 1.22
require github.com/yourorg/monorepo/pkg/database v0.0.0
// services/api/main.go
package main
import (
"context"
"fmt"
"log"
"github.com/yourorg/monorepo/pkg/database"
)
func main() {
store := database.NewMemoryStore()
user, err := store.GetUser(context.Background(), "1")
if err != nil {
log.Fatal(err)
}
fmt.Printf("User: %s\n", user.Name)
}
With the go.work file in the monorepo root, go build ./services/api/... resolves the database import locally. You can edit both packages in the same editor session and see changes immediately.
Running Commands in Workspace Mode
Most go commands work in workspace mode:
go build ./... # Builds all modules in the workspace
go test ./... # Tests all modules
go vet ./... # Vets all modules
go run ./services/api # Runs a specific module
To synchronize dependencies across workspace modules:
go work sync
This updates each module’s go.mod to use consistent dependency versions, which prevents version conflicts between workspace modules.
Disabling Workspace Mode
Sometimes you need to build a single module as if the workspace does not exist, for example to verify it works independently:
GOWORK=off go build ./...
Setting GOWORK=off disables workspace mode for that command. This is useful in CI to verify each module builds on its own.
Should You Commit go.work?
This depends on your team structure:
Commit it when the workspace represents the canonical development setup and everyone on the team develops all modules together. This is common in monorepos where the workspace boundary matches the repository boundary.
Do not commit it when different developers work on different subsets of modules, or when the workspace includes modules from other repositories. In this case, add go.work and go.work.sum to .gitignore and let each developer create their own workspace.
A middle-ground approach is to commit a go.work file for CI that includes all modules, ensuring the entire workspace builds and tests together.
Workspace Mode in CI
For CI pipelines in a monorepo, the workspace ensures all modules are tested with compatible local versions:
# .github/workflows/ci.yml
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- run: go work sync
- run: go test ./...
- run: go vet ./...
For independent module CI, disable workspace mode:
test-api:
runs-on: ubuntu-latest
defaults:
run:
working-directory: services/api
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- run: GOWORK=off go test ./...
Limitations
Workspace mode does not replace proper module versioning for published libraries. When you release pkg/database as a standalone module, consumers outside the workspace still resolve it through the module proxy. The workspace only affects local development.
The go.work file does not support replace directives. If you need replaces, they stay in individual go.mod files. However, workspace mode usually eliminates the need for development-time replaces.
Wrap Up
Go workspace mode removes the friction of multi-module development. A single go.work file tells the toolchain which modules live locally, replacing fragile replace directives with a clean, purpose-built mechanism. Use it in monorepos to develop shared packages alongside their consumers, run workspace-wide tests and builds, and maintain independent go.mod files that remain valid for standalone use.
Related articles
- Go Go Modules and Package Management
A practical guide to Go modules: go.mod, go.sum, semantic versioning, replace directives, and the commands you actually need day to day.
- Go Go Packages and Modules with go.mod
A practical tour of Go packages and modules: go mod init, imports, internal packages, semantic versioning, and the replace directive for local development.
- Go Go Concurrency: Fan-Out, Pipeline, and Worker Pool
Master advanced Go concurrency patterns including fan-out/fan-in, pipelines, and worker pools with practical examples and production-ready code.
- Go Go Database Patterns with sqlx and pgx
Master Go database patterns using sqlx and pgx for PostgreSQL including connection pooling, transactions, batch operations, and repository design.