Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
4: Go Workspaces
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 functionValidateEmail(email string) boolthat returnstrueif 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 amain.gothat importsvalidator-liband calls theValidateEmailfunction. - The Core Task: Do not use
replaceingo.mod. Instead, go to theworkspace-projectroot and usego work initandgo work useto link both modules. - Run the
user-service. Once it works, change the logic invalidator-lib(e.g., make it check for a "." as well) and verify thatuser-servicereflects this change immediately upon the next run.
There are no comments for now.