Go projects often start as a single module. As an application grows, however, its code may be divided into several modules to make development, testing, and maintenance easier.
For example, a company might maintain a backend API, a shared logging library, and an authentication library in separate Go modules. Each module has its own go.mod file and can be versioned independently.
The challenge appears when you need to develop these modules together.
Suppose your backend API depends on a shared library. You make a change to the library and want to test it immediately in the API. Without an appropriate development setup, you may need to publish a new library version, update the API's dependency, or manually change its module configuration.
These steps can slow down development and make it harder to test changes across related projects.
Go workspaces provide a solution. A workspace allows you to work with multiple local modules together without permanently changing their dependency versions or publishing every change before testing it.
In this article, you will learn how Go workspaces work, how to create a workspace, how to develop multiple modules together, how to run tests across modules, and how to avoid common dependency-management problems.
What Is a Go Workspace?
A Go workspace is a development environment that allows multiple Go modules to be used together.
Go introduced workspace support in Go 1.18. A workspace is commonly defined by a go.work file, which identifies the local modules that should participate in workspace mode.
Consider the following project structure:
go-projects/
├── go.work
├── user-service/
│ ├── go.mod
│ └── main.go
├── auth-library/
│ ├── go.mod
│ └── auth.go
└── logger-library/
├── go.mod
└── logger.go
Each directory is an independent Go module.
The user-service module can depend on auth-library and logger-library. During development, the workspace allows Go to resolve those dependencies from their local directories.
This means you can modify the authentication library, run the user service, and test the combined changes without publishing a new authentication library version.
A workspace does not merge the modules into one module. Each module keeps its own go.mod, dependencies, and module path.
The workspace simply tells Go which local modules should be considered together during development.
Go Workspace vs. Go Module
Before creating a workspace, it is useful to understand the difference between a module and a workspace.
A Go module defines a collection of related Go packages with its own dependency configuration. A workspace brings multiple local modules into a shared development environment.
Feature | Go module | Go workspace |
|---|
Main configuration | go.mod
| go.work
|
Purpose | Defines a module and its dependencies | Uses multiple local modules together |
Number of modules | Represents one module | Can include multiple modules |
Dependency management | Records module dependencies and versions | Can override dependency resolution for local workspace modules |
Typical use | Building and distributing a library or application | Developing related modules together |
Publishing | Modules can be versioned and published independently | A workspace itself is not a replacement for module publishing |
For example, a shared library might have this module declaration:
module example.com/company/auth-library
go 1.22
The module path identifies the library to other Go modules.
Its own go.mod remains responsible for the library's dependency requirements. A workspace does not eliminate the need to maintain this file.
Create a Workspace with Multiple Modules
Let's build a simple example with two modules:
Step 1: Create the Project Directories
Create the following directory structure:
go-workspace-demo/
├── user-service/
└── auth-library/
Open a terminal and create the directories:
mkdir go-workspace-demo
cd go-workspace-demo
mkdir user-service
mkdir auth-library
You can create these directories manually if you prefer.
Step 2: Initialize the Authentication Module
Move into the authentication library directory:
cd auth-library
go mod init example.com/company/auth-library
This creates a go.mod file similar to the following:
module example.com/company/auth-library
go 1.22
The Go version in your generated file depends on the installed Go toolchain and the command you use.
Next, create a file named auth.go:
package auth
import "strings"
func IsValidToken(token string) bool {
return strings.TrimSpace(token) != ""
}
This package provides a simple function that checks whether a token contains non-whitespace characters.
This function is only an example of module integration. It does not implement secure token validation for a real authentication system.
Step 3: Initialize the User Service Module
Return to the project root and enter the service directory:
cd ../user-service
go mod init example.com/company/user-service
This creates a separate go.mod file for the application.
Create main.go:
package main
import (
"fmt"
"example.com/company/auth-library"
)
func main() {
token := "sample-token"
if auth.IsValidToken(token) {
fmt.Println("Token is present")
return
}
fmt.Println("Token is missing")
}
The import path matches the authentication library's module path.
However, the library exists only on your local machine and has not been published. To make both modules work together during development, you can configure a workspace.
Create the go.work File
Return to the project root:
cd ..
Initialize a workspace:
go work init ./user-service ./auth-library
Go creates a go.work file that identifies the two local modules.
The generated file will resemble:
go 1.22
use (
./auth-library
./user-service
)
The exact Go version may differ depending on your environment.
The use directive tells Go that both directories belong to the workspace.
When workspace mode is active, Go can resolve the example.com/company/auth-library dependency from the local auth-library module.
You do not need to publish the library first or add a permanent local filesystem replacement to the service's go.mod.
How Workspace Dependency Resolution Works
Without a workspace, the service normally resolves dependencies according to its module requirements and the configured module sources.
With the workspace enabled, Go considers the modules listed in go.work. If a dependency corresponds to a module included in the workspace, Go can use that local module during development.
This behavior is especially useful when you are changing both the service and the library.
For example, you can modify auth.go, save the file, and rerun the service. The application uses the updated local code without requiring a new published library version.
Run the Application Using the Workspace
From the project root, run:
go run ./user-service
The application should print:
Token is present
The result confirms that the user service can import and execute code from the local authentication module.
You can also build the service:
go build -o user-service-app ./user-service
The generated executable is created in the current directory.
If the build fails, verify that both modules are listed in go.work, that the import path matches the library's module declaration, and that the required package and function are exported.
In Go, identifiers beginning with an uppercase letter are exported and can be accessed from other packages. That is why the example function is named IsValidToken.
Test Multiple Modules in the Workspace
A workspace becomes particularly useful when you need to test changes across related modules.
You can run tests from the workspace root:
go test ./...
This command tests packages matched by the current directory pattern. It does not automatically mean that every module listed in go.work will be tested as a separate module.
When you need to test every workspace module, use an explicit pattern that matches the module directories:
go test ./user-service/... ./auth-library/...
This command is useful when the workspace contains modules in separate subdirectories.
You can also run tests from an individual module directory:
cd auth-library
go test ./...
This tests the authentication library independently of the service.
Testing each module separately remains important. A workspace can make cross-module development easier, but it should not hide problems that would appear when a module is built and tested on its own.
Add a Unit Test to the Authentication Library
Create auth_test.go inside the auth-library directory:
package auth
import "testing"
func TestIsValidToken(t *testing.T) {
tests := []struct {
name string
token string
want bool
}{
{
name: "valid token",
token: "abc123",
want: true,
},
{
name: "empty token",
token: "",
want: false,
},
{
name: "whitespace token",
token: " ",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := IsValidToken(tt.token)
if got != tt.want {
t.Fatalf(
"IsValidToken(%q) = %v, want %v",
tt.token,
got,
tt.want,
)
}
})
}
}
Run the tests from the workspace root:
go test ./user-service/... ./auth-library/...
The test verifies the library's behavior independently while allowing you to maintain the service and library in the same development environment.
For production code, add tests that reflect the actual authentication requirements, including invalid, expired, or incorrectly signed tokens where applicable.
Manage Workspace Modules
As a project grows, you may need to add or remove modules from the workspace.
Go provides commands for managing the go.work file.
Add a Module
Suppose you create another module named logger-library.
From the workspace root, run:
go work use ./logger-library
Go adds the module to the workspace configuration.
You can inspect the result:
cat go.work
The workspace file should now contain the additional module path.
On Windows, you can inspect the file using PowerShell:
Get-Content go.work
Add Multiple Modules
You can add multiple module directories in one command:
go work use ./user-service ./auth-library ./logger-library
This updates the workspace configuration to include the specified modules.
Remove a Module
If you no longer need a module in the workspace, edit go.work and remove its path from the use block.
You can also use:
go work edit -dropuse=./logger-library
The module directory and its go.mod file remain unchanged. The command only removes the module from the workspace configuration.
Inspect Workspace Configuration
Use the following command to print the active workspace file:
go env GOWORK
If Go is using a workspace file, the command normally returns its path.
If workspace mode is disabled, it can return an empty value.
You can also inspect the Go version and other relevant environment settings:
go version
go env GOWORK GOMOD GOPATH
These commands are useful when the application behaves differently across development machines or CI environments.
Understanding GOWORK and Workspace Mode
Go determines whether workspace mode is active based on its environment and workspace configuration.
The GOWORK environment variable can control which workspace file Go uses.
For example, you can explicitly point Go to a workspace file:
GOWORK=/path/to/project/go.work go test ./...
This example uses Unix-style environment variable syntax. In PowerShell, use:
$env:GOWORK = "C:\path\to\project\go.work"
go test ./...
To disable workspace mode for a command, set GOWORK=off:
GOWORK=off go test ./...
On PowerShell:
$env:GOWORK = "off"
go test ./...
Disabling the workspace is useful when you want to verify that a module builds using its own dependency configuration rather than relying on local workspace modules.
Remember that disabling workspace mode does not automatically publish local modules or update their dependency versions. If the service depends on a version that is not available through its normal module sources, the command may fail.
Go Workspaces vs. Replace Directives
Before workspaces were available, developers often used the replace directive in go.mod to point a dependency to a local directory.
For example:
module example.com/company/user-service
go 1.22
require example.com/company/auth-library v0.0.0
replace example.com/company/auth-library => ../auth-library
This tells Go to use the local directory instead of the specified module version.
The approach can work, but it introduces a local filesystem path into the module's dependency configuration.
If the replacement is committed accidentally, other developers and CI systems may fail because the relative directory does not exist in their environments.
A workspace can avoid this problem by keeping local development configuration in go.work, separate from the module's normal dependency requirements.
Consideration | Workspace | Replace directive |
|---|
Configuration file | go.work
| go.mod
|
Scope | Can include multiple local modules | Overrides a particular module path |
Local multi-module development | Designed for this use case | Can support local dependency development |
Dependency replacement | Uses local workspace modules | Can replace a module with another path or module |
Risk of committing local paths | Lower when go.work is excluded from version control | Possible if a local replacement is committed |
Best use | Coordinated development across related modules | Intentional dependency replacement when appropriate |
A workspace and a replace directive solve related but different problems. Workspaces are generally a convenient choice when several local modules need to be developed together. A replacement can still be appropriate for specific dependency-management requirements.
Common Go Workspace Problems and Their Solutions
Go Cannot Find a Local Module
If Go reports that it cannot resolve a module, first verify that the module is included in go.work.
Run:
go env GOWORK
Then inspect the workspace file and confirm that the module path in the importing code matches the module declaration in the dependency's go.mod.
For example, if the library declares:
module example.com/company/auth-library
the service should import the corresponding package path:
import "example.com/company/auth-library"
A mismatch between the module path and the import path can prevent the package from being resolved correctly.
The Application Works Locally but Fails in CI
One common reason is that the local machine uses a workspace containing modules that are not available in the CI environment.
For example, the local go.work file might include a module directory that was not checked out in the build pipeline.
Another possibility is that the local build relies on a dependency replacement or uncommitted change.
To identify the issue, test the service with workspace mode disabled:
GOWORK=off go test ./...
If the command fails, inspect the module's dependency requirements and verify that the dependencies are available without the workspace.
A Workspace Change Affects More Modules Than Expected
Workspace mode can change dependency resolution for modules listed in go.work.
This is useful during development, but it can also make a module behave differently from how it would behave when consumed independently.
Test important modules both within the workspace and independently when preparing a release.
Dependency Versions Seem Different
A workspace can affect which local modules Go uses, but it does not mean every dependency automatically receives the newest version.
Inspect the module requirements and the active workspace configuration:
go list -m all
go env GOWORK
The first command displays module dependencies selected in the current module context. Run it from the module directory you want to inspect.
Compare the results with workspace mode enabled and disabled if dependency resolution is unexpected.
The Workspace File Contains Unintended Changes
A workspace file can be generated or edited as modules are added and removed.
Review changes to go.work before committing them. Teams should agree on whether the workspace file belongs in version control, based on their development workflow.
A workspace file that contains machine-specific paths or references to unavailable directories can cause confusion for other developers.
Best Practices for Go Workspaces
Keep Each Module Independently Buildable
A workspace should make development easier without making each module dependent on the workspace for basic correctness.
Maintain a valid go.mod in every module, and test important modules independently before publishing them.
Use Workspaces for Coordinated Development
Workspaces are most useful when modules are closely related and changes often need to be tested together.
They are less necessary when modules rarely change together or when developers consume only released dependency versions.
Keep Local Development Configuration Separate
Use the workspace to coordinate local modules rather than adding temporary filesystem replacements to dependency configuration without a clear reason.
Review go.work changes before sharing them with the team.
Test the Release Configuration
Before releasing a library, test it without relying on workspace-only modules.
This helps identify missing dependencies, incorrect module declarations, and code that works locally but fails for downstream consumers.
Avoid Treating a Workspace as a Dependency Versioning System
A workspace is not a replacement for semantic versioning or dependency management.
When a module is ready for use by other projects, publish or otherwise distribute it according to your team's release process, and update consumers to use the intended version.
Document the Workspace Setup
For repositories containing multiple modules, document how to initialize the workspace, run tests, and build individual modules.
This reduces setup errors for new developers and helps keep local development consistent across the team.
Advantages and Limitations of Go Workspaces
Go workspaces provide several benefits:
Developers can modify multiple modules without publishing each intermediate change.
Local changes to shared libraries can be tested immediately by dependent applications.
Related modules can be developed and debugged together.
The go.work file keeps workspace configuration separate from each module's dependency requirements.
Teams can test coordinated changes before deciding which module versions to release.
However, workspaces also have limitations:
Workspace dependency resolution can differ from the resolution used by a consumer outside the workspace.
A local workspace can hide missing dependency versions or incomplete release configuration.
Teams need to manage the workspace file carefully when modules live in separate repositories or directories.
A workspace does not replace independent module testing, versioning, or publishing.
Developers must understand which workspace file is active when debugging dependency issues.
These limitations are manageable when workspaces are treated as development tools rather than as a substitute for normal module dependency management.
Conclusion
Go workspaces provide a practical way to develop and test multiple Go modules together. By defining a go.work file, you can use local versions of related modules without publishing every change or permanently modifying each module's dependency configuration.
The basic workflow is straightforward: initialize each module, create a workspace with go work init, add modules with go work use, and run builds and tests against the combined project.
For production-ready development, continue testing modules independently, verify dependency resolution with workspace mode disabled when appropriate, and maintain a clear release process for publishing module versions.
When used correctly, Go workspaces make coordinated development easier while allowing each module to remain an independently maintained and distributable component.
Join the conversation! Your thoughts help the community grow.