So you're building a Go project. It starts simple, as they all do. Then it grows. Now you have a main API service, a separate service for handling background jobs, and you've been a good developer and put all your common code into a shared pkg
directory. Things like authentication helpers, database models, all the stuff both services need.
This is a great pattern. Until you need to change something in the pkg
directory.
Now you're stuck. You make a change in pkg
, but your API service doesn't see it. Why? Because its go.mod
file is pointing to the version on GitHub, not your local version. So you add a replace
directive. Then you have to do the same thing for your worker service. And you have to remember to take them out before you commit, otherwise, you'll break the build for everyone else.
It's a total mess and super easy to forget.
There Is a Better Way, and It's Called Workspaces
Go workspaces are the official, built-in solution to this exact problem. It's a simple feature that tells the Go compiler, "Hey, when you're building my project, I want you to use these specific modules from my local disk, not the ones from the internet."
You do this with a single file in the root of your project called go.work
. This file tells Go which local modules are part of your active development "workspace."
Let's look at how this works with our example. Imagine you have a project structured like this:
/my-cool-project
|-- /app
|-- go.work
|-- /service-core
| |-- go.mod
|-- /service-admin
| |-- go.mod
|-- /pkg
|-- /auth
|-- /str
Both service-core
and service-admin
need code from the pkg
directory. Instead of messing with replace
directives, your go.work
file would just look like this:
use (
./service-core
./service-admin
./pkg
)
And... that's it. Seriously.
Now, whenever you're working inside the /app
directory, Go automatically knows that if service-core
imports something from pkg
, it should use the pkg directory right there on your hard drive. No go.mod
changes needed. It's seamless.
Getting Started Is Easy
You can start using this in your projects today.
Go to the root of your project where your services live.
Run
go work init ./service-one ./service-two ./my-shared-lib
. This creates thego.work
file.If you add a new service later, just run
go work use ./new-service
.
It's a small change to your workflow that makes a huge difference.
Don't Forget to Sync Your Work
The go work sync
command updates the go.mod
files for every module inside your workspace.
While the go.work
file tells Go to use your local modules for development, your go.mod
files are what define the real dependencies for your CI/CD pipelines and for other developers.
go work sync
synchronizes the dependencies from your workspace back to each module's go.mod
file. This ensures they are accurate and up-to-date before you commit your changes, preventing broken builds for your team.
You should run it from your project root after you've finished your local changes:
go work sync
Why You Should Be Using This
To put it simply, workspaces make developing with multiple modules and shared libraries a breeze.
- You can finally make changes in a shared library and see them instantly in your other services.
- Your
go.mod
files stay clean and only describe your real, committed dependencies. - You'll never accidentally commit a
replace
directive again.
If you want to see a project that's built from the ground up using this philosophy, you should check out my Go CLI builder - GoFast. It uses workspaces to manage its core services and shared packages, making it a great real-world example of how to build scalable, maintainable Go applications. And MUCH more :).
Hope you enjoyed it, have a good day!
Top comments (0)