07 - Multi-Module with go work

📋 Jump to Takeaways

Your bookmarks API uses errx for traced errors and slogr for pretty logging. These aren't specific to bookmarks — they're general-purpose tools you'd use in any Go project. So you extract them into their own repository and publish them as a module. Now you have two repos:

  • github.com/yourname/bookmarks — the API
  • github.com/yourname/gokit — shared libraries (errx, slogr)

This works great until you need to change both at the same time. You're adding a new function to errx and updating the bookmarks API to use it. Without workspaces, this is painful.

The Problem: replace Hacks

The naive approach is a replace directive in go.mod:

// bookmarks/go.mod
module github.com/yourname/bookmarks

require github.com/yourname/gokit v0.2.0

// Temporary — DO NOT COMMIT
replace github.com/yourname/gokit => ../gokit

This points Go to your local copy of gokit instead of the published version. It works, but:

  • You have to remember to remove it before committing
  • If you forget, CI breaks — it can't find ../gokit
  • Every developer needs the same directory layout on their machine
  • It's per-module — if you have five services using gokit, that's five replace directives to manage

The Solution: go work

Go workspaces solve this. From a parent directory that contains both repos:

projects/
├── go.work
├── bookmarks/
│   ├── go.mod          ← module: github.com/yourname/bookmarks
│   ├── main.go
│   ├── handler.go
│   └── store.go
└── gokit/
    ├── go.mod          ← module: github.com/yourname/gokit
    ├── errx/
    │   └── errx.go
    └── slogr/
        └── slogr.go

Set it up:

cd projects
go work init
go work use ./bookmarks ./gokit

This creates a go.work file:

go 1.24

use (
    ./bookmarks
    ./gokit
)

Now when you run go build or go test in bookmarks/, Go uses your local gokit instead of the published version. No replace directives. Both go.mod files stay clean and committable.

Importing Across Modules

Your bookmarks/go.mod still has the normal require:

module github.com/yourname/bookmarks

go 1.24

require github.com/yourname/gokit v0.2.0

And your imports look the same as always:

package main

import (
    "github.com/yourname/gokit/errx"
    "github.com/yourname/gokit/slogr"
)

func main() {
    slogr.SetupLog(slogr.LevelDebug, slogr.TypePretty)
    // ...
}

The workspace silently redirects github.com/yourname/gokit to the local ./gokit directory. Your code doesn't know or care — it uses the same import paths either way.

The Workflow

Here's how you develop across both repos:

  1. Edit gokit — add a new function, fix a bug
  2. Edit bookmarks — use the new function immediately, no publishing needed
  3. Test bothgo test ./... from the workspace root runs tests across all modules
  4. When ready to ship:
    • Commit and tag gokit with a new version (e.g., v0.3.0)
    • Push gokit to its repo
    • Update bookmarks/go.mod: go get github.com/yourname/[email protected]
    • Remove the workspace (or just don't use it in CI)

The workspace is scaffolding for development. go.mod with proper versions is the permanent record.

go.work Is Not Committed

Add go.work and go.work.sum to .gitignore in each repo. The workspace is a local development tool — it shouldn't be in version control because:

  • It contains local paths that differ per developer
  • CI should test against published versions, not local overrides
  • Each developer sets up their own workspace based on what they're working on

In CI, set GOWORK=off to make sure your modules build correctly against their published dependencies:

GOWORK=off go test ./...

Running and Building

From the workspace root (where go.work lives):

# Run the API
go run ./bookmarks

# Run tests across all modules
go test ./...

# Build
go build -o bookmarks-api ./bookmarks

The workspace handles module resolution automatically.

When to Use Workspaces

Workspaces are for when you're developing separate modules at the same time:

  • A library in one repo and an app that uses it in another
  • Multiple microservices that share a common module
  • Contributing to an open-source library while testing changes in your own project

You don't need workspaces when all your code is in one repo with one go.mod. Use sub-packages instead — that's simpler and what Go is designed for.

Key Takeaways

  • go work init + go work use creates a workspace for developing across separate modules
  • Workspaces redirect imports to local directories without touching go.mod
  • Don't commit go.work — it's a local development tool
  • Use GOWORK=off in CI to test against published versions
  • When done developing, tag and publish the library, update go.mod, move on
  • If your code is in one repo, you don't need workspaces — use sub-packages

🚀 Ready to run?

Complete examples for this lesson. Copy and run locally.

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

© 2026 ByteLearn.dev. Free courses for developers. · Privacy