Visual Go Tutorial

Multi-Module Workspaces

Develop across multiple Go modules simultaneously without publishing. A visual companion to the official Go tutorial.

Go 1.18+ · ~15 min read · 6 steps
The problem

Why workspaces?

When you develop a library and its consumer at the same time, Go normally fetches the library from the module cache — the published version. To use local changes, you'd add replace directives in go.mod. That's fragile and easy to accidentally commit. Workspaces solve this cleanly.

Without workspaces

Manual replace directives in go.mod. Easy to commit by mistake. One module at a time.

With workspaces

A go.work file at the root. Never committed. Multiple modules resolve locally and automatically.

Step 01

Create a module

Start by setting up a directory and initializing a new Go module that imports an external package.

$ mkdir workspace && cd workspace
$ mkdir hello && cd hello
$ go mod init example.com/hello
go: creating new go.mod: module example.com/hello
$ go get golang.org/x/example/hello/reverse

Now create the entry point that uses the reverse package:

hello/hello.go Go
package main

import (
    "fmt"

    "golang.org/x/example/hello/reverse"
)

func main() {
    fmt.Println(reverse.String("Hello"))
}
$ go run .
olleH
workspace/ hello/ module: example.com/hello hello.go Module cache golang.org/x/.../reverse imports Downloaded via go get
Your module imports reverse from the remote module cache
Step 02

Initialize the workspace

Go back to the workspace root and create a go.work file. This tells Go which modules belong to your local workspace.

$ cd workspace
$ go work init ./hello

This generates a go.work file:

workspace/go.work Go Work
go 1.18

use ./hello
The go.work syntax mirrors go.mod. The use directive lists local module directories. The go directive sets the minimum Go version.
Step 03

Run from the workspace root

With the workspace active, you can build and run any module from the root directory — Go resolves all use-listed modules automatically.

$ go run ./hello
olleH
i
Running go run ./hello from outside the workspace (where no go.work exists) would fail — Go wouldn't know which modules to use.
Step 04

Add a local dependency

Clone the dependency's repository and add it to the workspace. Now Go resolves imports from your local copy instead of the module cache.

$ git clone https://go.googlesource.com/example
Cloning into 'example'...
$ go work use ./example/hello

The go.work file now includes both modules:

workspace/go.work Go Work
go 1.18

use (
    ./hello
    ./example/hello
)
workspace/
go.work
hello/
go.mod
hello.go
example/
hello/
go.mod
reverse/
workspace/ go.work use ./hello, ./example/hello hello/ Your code example/hello/ Local clone imports reverse resolved locally Module cache bypassed Key insight Edit locally, see changes instantly
The workspace resolves reverse from your local clone, not the module cache
Step 05

Modify and use the dependency

Add a new function to the cloned reverse package and immediately use it from your hello module — no publishing required.

Create a new file in the cloned dependency:

example/hello/reverse/int.go Go
package reverse

import "strconv"

// Int returns the decimal reversal of the integer i.
func Int(i int) int {
    i, _ = strconv.Atoi(String(strconv.Itoa(i)))
    return i
}

Update your module to call the new function:

hello/hello.go Go
package main

import (
    "fmt"

    "golang.org/x/example/hello/reverse"
)

func main() {
    fmt.Println(reverse.String("Hello"), reverse.Int(24601))
}
$ go run ./hello
olleH 10642
hello/hello.go reverse.String("Hello") reverse.Int(24601) ← new reverse/int.go func Int(i int) int { // reverses digits } calls Int() $ go run ./hello olleH 10642
Cross-module changes work instantly — no version bump or publish step
The go.work file acts as a local override layer. Since both modules are in the same workspace, changes in one are immediately visible to the other during builds.
Step 06

Release and clean up

When you're done developing, release the modified dependency, pin the version in your go.mod, and retire the workspace.

A Tag the dependency git tag v0.1.0 && git push on the example/hello repo B Update your go.mod cd hello go get .../hello@v0.1.0 C Workspace retired go.work no longer needed Which module gets tagged? hello/ — your app no tag needed example/hello/ — the library ← tag this one v0.1.0 hello/go.mod now requires v0.1.0 go.work = temporary dev tool go.mod = permanent, portable dependency record
Tag the dependency you modified, update your go.mod, and the workspace is no longer needed
!
Don't commit go.work to version control. It's a local development convenience — your teammates might have different directory layouts. The go.mod file is what gets committed and shared.
Reference

Workspace commands

go work init ./mod Create a go.work file with one or more initial modules
go work use ./mod Add another module directory to the workspace. Use -r to scan subdirectories recursively
go work edit Edit the go.work file programmatically, similar to go mod edit
go work sync Sync workspace build list dependencies into each module's go.mod
Summary

The full lifecycle

Develop go.work + local clones Test Changes are instant Release git tag + go get @ver Clean up go.work retired go.mod carries the pinned version permanently
Content adapted from go.dev. Original material © Google, licensed under Creative Commons Attribution 4.0. Visual tutorial design by Arafat.