Go Workspaces and Vendoring
Reviewed & published by Brayan K
Real projects span multiple modules and must build the same way everywhere. You'll use go work init/use and the go.work file to develop modules together, compare workspaces with replace, vendor dependencies with go mod vendor, and lock things down with GOFLAGS=-mod=vendor, GOPROXY, and go.sum for reproducible builds.
Part of the free Go course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- Create a workspace with go work init and go work use
- Understand the go.work file and whether to commit it
- Choose workspaces vs replace directives
- Vendor dependencies with go mod vendor
- Force vendored builds with GOFLAGS=-mod=vendor
- Use GOPROXY and go.sum for reproducible builds
1️⃣ Creating a workspace
go work init creates a go.work file and go work use adds local module directories. Now one module builds against your local copy of another — no replace lines, no publishing first.
# You have two local modules you want to develop together:
# ./app (imports the helper)
# ./hello (a library module)
# 1. From the parent directory, create a workspace.
$ go work init ./app
# 2. Add the second module to the workspace.
$ go work use ./hello
# Now ./app builds against your LOCAL ./hello — no go.mod edits,
# no publishing ./hello first. Edit hello, rebuild app, see it instantly.
$ go run ./app2️⃣ The go.work file
The go.work file lists member modules in a use block. It describes a local layout, so teams often gitignore it. Check the active file with go env GOWORK.
// go.work — sits ABOVE the member modules.
// It is a local development convenience; teams often gitignore it
// because it reflects one person's directory layout.
go 1.22
use (
./app
./hello
)
// You can still pin a dependency for the whole workspace if needed:
// replace example.com/foo => ../foo3️⃣ Workspaces vs replace
A replace directive edits the committed go.mod and can leak a local path into a release. A workspace keeps that wiring in go.work, so go.mod stays clean and publishable.
// OLD WAY — a replace directive inside app/go.mod.
// This edits (and usually commits) the module file, which can leak a
// local path into a release if you forget to remove it.
module example.com/app
go 1.22
require example.com/hello v0.0.0
replace example.com/hello => ../hello // local wiring in the COMMITTED file
// WORKSPACE WAY — keep go.mod clean and put local wiring in go.work
// instead, so nothing local-only is committed to the module.4️⃣ Vendoring and reproducible builds
go mod vendor copies dependencies into vendor/ for self-contained, offline builds. GOPROXY controls where modules are fetched, and go.sum checksums lock their exact content.
# Vendoring copies every dependency into a local vendor/ directory,
# so builds are self-contained and need no network.
$ go mod vendor # create/refresh vendor/ from go.mod + go.sum
# If vendor/ exists and is consistent, go builds use it automatically
# (equivalent to -mod=vendor). Force it explicitly anywhere:
$ go build -mod=vendor ./...
$ GOFLAGS=-mod=vendor go test ./... # whole session uses vendor/
# Modules are normally fetched through a PROXY; pin it for your org:
$ go env -w GOPROXY=https://proxy.golang.org,direct
# go.sum checksums + pinned versions => byte-for-byte reproducible builds.🎯 Your Turn
Set up a workspace for two local modules. Fill in the two blanks marked ___ to create it and add the second module.
# 🎯 YOUR TURN — fill in the blanks marked ___
# You have ./api and ./shared as two local modules.
# 1) Initialize a workspace containing ./api
$ go work ___ ./api # 👉 init
# 2) Add the second local module to the workspace
$ go work ___ ./shared # 👉 use
# 3) Build, now resolving ./shared from your local copy
$ go build ./...
# ✅ Expected: ./api compiles against your LOCAL ./shared,
# with no replace directive and no go.mod edits.Common Errors (and the fix)
❌ Committing a local replace to go.mod — it can ship in a release.
✅ Use a go.work workspace for local wiring instead.
❌ Committing go.work by accident — forces your layout on everyone.
✅ Gitignore it; let each developer (and CI) resolve modules normally.
❌ Editing vendor/ by hand — it drifts from go.mod.
✅ Regenerate with go mod vendor after any dependency change.
❌ Deleting or distrusting go.sum — breaks reproducibility.
✅ Keep go.sum committed; it verifies every dependency's content.
🧠 Quick Recall
add a module to the workspace = ?
go work use ./mod. go work init creates the go.work file first.
what locks dependency content?
go.sum checksums (plus pinned versions and a stable GOPROXY or vendor/).
🎯 Mini-Challenge: A Reproducible Offline Build
Tidy, vendor, and build a module with GOFLAGS=-mod=vendor so a teammate or CI can build it with no network, getting identical dependencies via go.sum.
# 🎯 MINI-CHALLENGE: make a build fully self-contained and reproducible.
# 1. From a module, run: go mod tidy (lock the requirement set)
# 2. Run: go mod vendor (copy deps into vendor/)
# 3. Verify it builds offline using only vendor/:
# GOFLAGS=-mod=vendor go build ./...
# 4. Confirm go.sum + vendor/modules.txt are committed.
#
# ✅ Goal: a teammate (or CI) can build with NO network access,
# getting byte-for-byte the same dependencies via go.sum.
$ go mod tidy
$ go mod vendor
$ GOFLAGS=-mod=vendor go build ./...🎉 Lesson Complete!
- ✅ go work init/use build a go.work for multi-module local dev
- ✅ go.work is a local convenience — usually gitignored, not committed
- ✅ Prefer a workspace over a committed replace for local wiring
- ✅ go mod vendor + -mod=vendor give self-contained offline builds
- ✅ GOPROXY and go.sum make builds reproducible everywhere
- ✅ You've finished the Go course — you can build, test, profile, lint, and ship production Go.
Practice quiz
What problem do Go workspaces (Go 1.18+) solve?
- developing multiple local modules together without editing each go.mod
- compiling C code
- running tests in parallel
- downloading the standard library
Answer: developing multiple local modules together without editing each go.mod. A workspace lets you work on several modules at once locally, so changes in one are seen by another without replace edits.
Which command creates a go.work file?
- go work new
- go init work
- go work init
- go mod init
Answer: go work init. go work init [dirs...] creates a go.work file; go work use adds module directories to it.
What does go work use ./mymod do?
- downloads mymod
- adds the local module directory to the workspace
- deletes the module
- runs its tests
Answer: adds the local module directory to the workspace. go work use registers a local module directory in go.work so the workspace builds against your local copy.
Where does the go.work file usually live, and is it committed?
- in the home directory only
- it is generated at build time
- inside each module; always committed
- above the modules; often left OUT of version control as a local dev convenience
Answer: above the modules; often left OUT of version control as a local dev convenience. go.work typically sits above the member modules and is frequently gitignored since it reflects a local layout.
How do workspaces compare to a replace directive in go.mod?
- workspaces avoid editing/committing each go.mod; replace changes the module file itself
- replace is newer than workspaces
- workspaces only work in CI
- they are identical in every way
Answer: workspaces avoid editing/committing each go.mod; replace changes the module file itself. A workspace keeps local wiring out of go.mod; replace edits the committed module file, which can leak into releases.
What does go mod vendor create?
- a go.work file
- a vendor/ directory containing copies of all dependencies
- a compiled binary
- a test report
Answer: a vendor/ directory containing copies of all dependencies. go mod vendor copies the module's dependencies into a top-level vendor/ directory for self-contained builds.
When a vendor/ directory is present, how does the go command behave by default?
- it always re-downloads
- it errors out
- it ignores vendor/
- it builds using the vendored copies (-mod=vendor) automatically
Answer: it builds using the vendored copies (-mod=vendor) automatically. If vendor/ exists and is consistent, recent Go versions default to -mod=vendor and build from it without network access.
What does the GOFLAGS=-mod=vendor environment setting do?
- sets the Go version
- disables modules
- forces builds to use the vendor/ directory
- enables the race detector
Answer: forces builds to use the vendor/ directory. GOFLAGS=-mod=vendor makes every go command use vendored dependencies, useful in locked-down or offline CI.
What is GOPROXY?
- a linter
- the module proxy URL(s) the go command fetches modules through
- a build cache directory
- a test runner
Answer: the module proxy URL(s) the go command fetches modules through. GOPROXY (default proxy.golang.org,direct) controls where modules are downloaded from; you can set a private proxy.
Which practice most helps REPRODUCIBLE builds?
- pinning versions in go.mod/go.sum and using a proxy or vendor/ so the same dependencies resolve every time
- disabling go.sum
- always using the latest version
- editing go.mod by hand each build
Answer: pinning versions in go.mod/go.sum and using a proxy or vendor/ so the same dependencies resolve every time. go.sum checksums plus pinned versions (and a proxy or vendored copies) ensure everyone builds the exact same code.
Continue this course
- Previous: Tooling: go vet, fmt & golangci-lint
Frequently asked questions
What is a Go workspace and when do I want one?
A workspace (Go 1.18+) lets you develop several local modules together. You create a go.work file with go work init and add module directories with go work use; the go command then resolves those modules from your local copies instead of downloaded versions. It's ideal when you're changing a library and an app that uses it at the same time: edit the library, rebuild the app, and see the change immediately — no publishing, no go.mod edits.
What goes in go.work, and should I commit it?
go.work sits above the member modules and lists them in a use block (plus an optional go version and replace lines). It describes a local development layout, so teams commonly leave it out of version control (gitignore it) because each developer's paths may differ. CI usually builds the individual modules normally. You can inspect the active file with go env GOWORK.
How are workspaces different from a replace directive?
A replace directive lives inside a module's go.mod and is committed with it, which is fine for a deliberate, permanent override but risky for local development — a stray replace pointing at ../something can leak into a release. A workspace keeps that local wiring in go.work, outside any module file, so your go.mod stays clean and publishable. Use a workspace for multi-module local dev and replace for an intentional, committed substitution.
What does go mod vendor do and when should I use it?
go mod vendor copies all of your module's dependencies into a top-level vendor/ directory and writes vendor/modules.txt. When vendor/ is present and consistent, recent Go versions build from it automatically (as if -mod=vendor), so builds are self-contained and need no network. Vendoring suits locked-down CI, air-gapped environments, or teams that want every dependency reviewable in their own repo. The trade-off is a larger repository.
What are GOFLAGS=-mod=vendor and GOPROXY for?
GOFLAGS=-mod=vendor forces every go command in your shell or CI to use the vendor/ directory rather than the module cache or network — handy when you must guarantee offline, vendored builds. GOPROXY sets the module proxy the go command fetches through (default proxy.golang.org,direct); point it at a private proxy for an internal mirror, or include direct to fall back to fetching straight from version control.
How do I get reproducible builds?
Pin exact versions in go.mod and let go.sum record cryptographic checksums of every dependency; the go command verifies those checksums on download, so nobody can silently swap a dependency's contents. Combine that with a stable GOPROXY (or a committed vendor/ directory) so the same versions resolve everywhere. Run go mod tidy to keep the requirement set accurate. Together these guarantee that you, your teammates, and CI build byte-for-byte the same code.