Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was maintaining a shared internal telemetry library while simultaneously building a new microservice that relied on it. Every time I found a bug in the library or wanted to add a new metric, I went through this exhausting cycle: make the change in the library, commit it, push it to a branch, go back to the microservice, and run go get to pull that specific branch. If I didn't want to push half-finished code to Git, I'd use a replace directive in my go.mod file to point to a local folder on my hard drive. The problem? I'd inevitably forget to remove that replace line before committing, and suddenly the CI/CD pipeline would crash because the build server didn't have a folder at /Users/myname/dev/telemetry.

That's exactly the headache Go Workspaces were designed to solve. They let you tell the Go toolchain, "For this session, I'm working on these three different modules at once; treat them as one big project," without ever touching your go.mod files.

The Friction of Local Dependencies

Before workspaces, if you had Module A depending on Module B, and both were on your local machine, Go still wanted to fetch Module B from a remote repository. The replace directive was our only escape hatch. It worked, but it was invasive. You were modifying the module's definition just to satisfy your local environment. It felt like a hack because it was one.

With Go Workspaces, the "truth" about where a module lives is moved out of go.mod and into a separate file called go.work. This file stays on your machine and is almost always added to your .gitignore. It acts as a coordinator, telling Go to look in multiple local directories for modules before trying to go to the internet.

Orchestrating Modules with go.work

Setting this up is surprisingly lightweight. If you have a project structure where your API and your shared library live in sibling folders, you just initialize a workspace in the root directory containing both. When you run go work init followed by go work use ./path/to/module, Go creates a go.work file that lists those paths.

// Example go.work file
go 1.21

use (
    ./my-api
    ./my-shared-lib
)

Now, when you're inside my-api and you call a function from my-shared-lib, Go sees that both are part of the same workspace. It will use the local version of the library automatically. You can change a line of code in the library, hit save, and immediately run your API tests to see the effect. No pushing to Git, no version bumping, and—most importantly—no accidentally committing local file paths to your repository. I've found this completely transforms the experience of developing "platform" code that supports other internal services.

Workspaces vs. Production Builds

It is crucial to remember that go.work is a development-time convenience. It is not meant to be distributed. When you build your final binary for production or run your CI pipeline, the go.work file isn't there. The Go toolchain will fall back to the versions specified in your go.mod files.

This is a feature, not a bug. It ensures that your production build is reproducible and based on tagged releases, not whatever happened to be sitting in a folder on your laptop. If you find yourself needing a workspace in production, you've likely got a module boundary problem that needs a deeper look.




📋 Practical Task

Linking a Custom Validation Library to a User Service

In this exercise, you will simulate a real-world scenario where you are developing a shared validation library and a user service simultaneously using a Go Workspace.

Goal: Create two separate modules and link them using go.work so that changes in the library are reflected in the service without modifying go.mod.

Requirements:

  • Create a root directory named workspace-project.
  • Inside that directory, create a folder named validator-lib. Initialize it as a module (go mod init validator-lib) and create a function ValidateEmail(email string) bool that returns true if the string contains an "@" symbol.
  • Create a second folder named user-service. Initialize it as a module (go mod init user-service).
  • In user-service, write a main.go that imports validator-lib and calls the ValidateEmail function.
  • The Core Task: Do not use replace in go.mod. Instead, go to the workspace-project root and use go work init and go work use to link both modules.
  • Run the user-service. Once it works, change the logic in validator-lib (e.g., make it check for a "." as well) and verify that user-service reflects this change immediately upon the next run.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.