Demystifying Go Modules: From go.mod to go.local.mod
Go has come a long way from its early GOPATH-only days. If you've ever scratched your head over how Go manages dependencies, especially in a multi-repo setup, you're not alone. After a day of diving deep into Go modules, GOROOT/GOPATH, and custom workflows, here's a breakdown that could help you understand and adopt best practices around Go dependency management — especially if you’re working in a large or enterprise codebase.
One Go Version at a Time
Out of the box, Go assumes a single version installed on your system. All projects use this version unless you explicitly install and switch using a version manager like gvm or asdf.
If you’re juggling multiple projects with varying Go versions, a version manager is the way to go.
GOROOT vs GOPATH — Do You Still Need Them?
- GOROOT points to where Go itself is installed.
- GOPATH is your workspace for caching dependencies and installing tools.
With modules, you no longer need to keep your projects inside $GOPATH/src. However, GOPATH is still relevant:
$GOPATH/pkg/mod– stores your downloaded dependencies.$GOPATH/bin– stores binaries like linters, formatters, and other Go CLI tools.$GOPATH/src– still created for backward compatibility, though rarely used today.
You usually don’t need to set GOPATH or GOROOT manually unless you're doing something custom.
Introducing go.mod and go.sum
- Run
go mod init <module-name>to create a new module. - This makes your folder a Go module and allows you to manage dependencies declaratively
- Dependencies are downloaded to
$GOPATH/pkg/mod.
go.mod
- Declares the module name and direct dependencies.
go.sum
- Contains cryptographic checksums of dependencies.
- Prevents remote packages from being tampered with even if the version tag remains the same.
Similar to Python Virtual Environments
If you’re coming from the Python world, Go modules serve a role similar to virtual environments. Just like Python’s venv helps isolate dependencies per project to prevent version clashes, go.mod and go.sum allow Go projects to declare and lock down their dependencies — ensuring that each project is reproducible and stable, regardless of what other projects on your machine are using.
The key difference is that Go doesn’t create isolated folders with dependencies inside your project. Instead, it uses a global cache in $GOPATH/pkg/mod but manages isolation logically through the module system.
What About Indirect Dependencies?
When you run go mod tidy, it:
- Adds indirect dependencies that are needed at runtime.
- Removes unused dependencies.
These are marked with // indirect in go.mod and don’t need to be manually added.
Even if all transitive dependencies are listed, Go still recursively resolves the entire dependency graph during build and test time — ensuring that all packages and their sub-packages are accounted for.
However, it will use the exact versions pinned in go.mod (or the modfile in use). This means:
- No surprises.
- No silent upgrades.
- Everything is reproducible.
This behavior is key to Go’s stability and repeatable builds.
Pro Tip: Speed Up Builds with go mod tidy
Running go mod tidy before building your project has a side benefit — it can speed up your first build. Since it resolves and downloads all required dependencies upfront, go build won’t need to pause and fetch things later. This is especially useful in CI pipelines or fresh environments.
The Case for go.local.mod
In enterprise or multi-repo setups, dependencies often live in other internal repositories. These dependencies change frequently, and committing them into go.mod would require constant updates.
Get Amogh Vardhan Kashi’s stories in your inbox
Join Medium for free to get updates from this writer.
Instead:
- Keep
go.modclean with only direct dependencies. - Dynamically generate a
go.local.modduring the build (usually viamakeor similar tools). - This file includes transitive, development, and testing dependencies (like linters, test tools).
- Use
replacedirectives ingo.local.modto point to local clones of other internal repos.
Example:
replace example.com/internal/module => ../moduleWhy Use go.local.mod for Dev Tools and Codegen
Another powerful use of go.local.mod is for tools needed only in development or testing — especially code generation tools.
Why not put these in go.mod?
- Tools like
mockgen,stringer,protoc-gen-go, etc., are used only during build or test phases. - Adding them to
go.modclutters your production dependencies. - Frequent updates across repos can cause unnecessary changes and conflicts.
go.local.mod to the rescue:
You can declare such tools only in go.local.mod, and use them with the -modfile flag:
Using -modfile=go.local.mod
The Go toolchain supports a -modfile flag:
go build -modfile=go.local.modThis allows you to override go.mod with your custom mod file without affecting the repo's main go.mod.
Making It Work in VS Code
If you use VS Code for Go development:
To avoid red squiggles and import errors:
"go.buildFlags": ["-modfile=../go.local.mod"]
"go.testFlags": ["-modfile=${workspaceFolder}/go.local.mod"]📌 Note: VS Code doesn’t interpolate ${workspaceFolder} during build, so absolute path is mentioned.
Bonus Tools
go mod edit– Programmatically editgo.mod(e.g., add/remove/replace dependencies).go mod graph– Visualize the full dependency graph.
Final Thoughts
Go’s module system is both powerful and flexible, but it requires some understanding when working in large or multi-repo environments. By using go.local.mod smartly and configuring your tools correctly, you can ensure clean builds, reduce merge conflicts, and avoid surprises from changing dependencies.
✍️ Disclaimer
This article was created in collaboration with ChatGPT to document and distill learnings from a deep dive into Go modules, dependency management, and developer tooling. The goal was to understand the ecosystem through an exploratory, conversational process — turning common questions into a concise and developer-friendly reference.




