go mod init — Find the Bug¶
Each snippet contains a real-world bug related to initialising and maintaining Go modules. Find it, explain it, fix it.
Bug 1 — Uppercase letters in module path¶
Bug: Module paths are case-sensitive and the proxy normalises differently from Git. Uppercase characters force !-escaping in $GOPATH/pkg/mod (github.com/!acme/!my!app) and break case-insensitive filesystems on macOS/Windows. Two collaborators with different OSes will produce different go.sum entries.
Fix: use a lowercase path and rename the GitHub repo if needed:
Bug 2 — Missing dot in module path¶
Bug: Without a dot in the first path element, Go treats the module as private/local. Anyone trying to go get myproject fails with "malformed module path: missing dot in first path element." You can never publish, import from another project, or use replace cleanly.
Fix: use a real domain (or a known sentinel like example.com):
For throwaway scratch work, example.com/myproject is acceptable.
Bug 3 — Running go mod init in the wrong directory¶
Bug: The module was initialised inside cmd/server, not at the project root. Now cmd/server is its own module. Imports like github.com/me/myapp/internal/db resolve to nothing because the rest of the tree is outside the module.
Fix: delete the misplaced go.mod, go to the project root, and re-init:
Bug 4 — Re-running go mod init and losing dependencies¶
$ ls
go.mod go.sum main.go
$ go mod init github.com/me/myapp # I just want to "reset" it
go: /Users/me/code/myapp/go.mod already exists
$ rm go.mod go.sum
$ go mod init github.com/me/myapp
$ go build
main.go:5:8: no required module provides package github.com/spf13/cobra
Bug: Deleting go.mod and go.sum to "reinitialise" wipes every require, replace, and pinned version. The next build re-resolves everything from scratch — dropping pins, breaking reproducibility, possibly pulling in newer (or yanked) versions.
Fix: never re-init an existing module. To change the path, edit go.mod directly or run:
To refresh the dep graph without losing pins:
Bug 5 — Missing go directive¶
Bug: No go directive. go mod init always writes one, so this file was hand-edited or generated by an old/broken tool. Without it, the toolchain assumes go 1.16 semantics, which disables module-graph pruning and lazy loading. Builds slow down and behaviour subtly differs.
Fix: declare your minimum Go version:
Bug 6 — go directive higher than installed toolchain¶
Bug: Either someone bumped go 1.22 to go 1.99 to "future-proof" it, or copy-pasted from an unrelated module. The CI image only has Go 1.22 installed, so every pipeline now fails.
Fix: set the directive to the minimum version you actually need, not the maximum you can imagine:
If you genuinely need a newer toolchain, also add:
so Go can auto-download it instead of erroring.
Bug 7 — Major-version path mismatch¶
$ git tag v2.0.0
$ git push --tags
$ # consumer
$ go get github.com/me/awesome@v2.0.0
go: github.com/me/awesome@v2.0.0: invalid version: module contains a go.mod
file, so major version must be compatible: should be v0 or v1, not v2
Bug: Semantic Import Versioning rule: any major version v2+ must include the major suffix in the module path. The go.mod still says github.com/me/awesome, so Go refuses to publish it as v2.
Fix: edit go.mod to include /v2 and update all internal imports:
Then re-tag:
Bug 8 — Two go.mod files in the same project¶
Bug: Someone ran go mod init inside internal/tools to "isolate" a script. Now internal/tools is a sub-module — packages from the outer module cannot import it via the normal path, and go build ./... from the root silently skips it.
Fix: decide intentionally. If internal/tools is meant to be part of the main module, delete its go.mod. If it should be separate (multi-module repo), use a go.work file at the root:
Bug 9 — replace directive leaking into a release¶
module github.com/me/api
go 1.22
require github.com/me/shared v1.4.0
replace github.com/me/shared => ../shared
Bug: The replace ... => ../shared worked locally because the developer had both repos checked out. Once tagged and published, every consumer fails with: "replacement directory ../shared does not exist." replace directives apply only to the main module — but they still poison releases by signalling untrusted state.
Fix: remove replace before tagging, or move it into a local-only go.work:
Commit go.mod without replace; keep go.work ignored or workspace-only.
Bug 10 — vendor/modules.txt out of sync¶
$ go build
go: inconsistent vendoring in /Users/me/code/myapp:
github.com/spf13/cobra@v1.8.0: is explicitly required in go.mod, but
not marked as explicit in vendor/modules.txt
Bug: Someone edited go.mod (added a require), or hand-edited vendor/, without re-running go mod vendor. The two views of the dep graph disagree.
Fix: regenerate the vendor tree:
Never edit vendor/modules.txt by hand.
Bug 11 — Module path with .git suffix¶
Bug: Most people copy the SSH/HTTPS clone URL (git@github.com:me/myapp.git or https://github.com/me/myapp.git) and reuse it as a module path. Go does not strip the .git, so importers must write import "github.com/me/myapp.git/foo" — ugly and inconsistent with everyone else's repos.
Fix: drop the .git:
Update internal imports accordingly.
Bug 12 — Trailing slash in module path¶
Bug: A trailing slash silently corrupts the path. Some tools accept it; others (the proxy, go list -m) treat the module as a different identity. Imports may resolve weirdly, and go.sum entries become unstable.
Fix: remove the trailing slash:
Bug 13 — Wrong-cased import path¶
$ go build
go: github.com/sirupsen/logrus@v1.9.0: github.com/Sirupsen/logrus@v1.9.0:
parsing go.mod: unexpected module path "github.com/sirupsen/logrus"
Bug: The repository was renamed from Sirupsen to sirupsen years ago. On case-insensitive filesystems, the old path appears to work locally, but the proxy and go.sum reject it.
Fix: update every import to the canonical lowercase form:
Then run go mod tidy.
Bug 14 — go.mod committed without go.sum¶
$ git ls-files | grep -E 'go\.(mod|sum)'
go.mod
$ go build
verifying github.com/spf13/cobra@v1.8.0: missing go.sum entry; to add it:
go mod download github.com/spf13/cobra
Bug: Someone added go.sum to .gitignore thinking it was a generated artifact. Without it, every clean checkout has to re-derive checksums and a malicious proxy could silently substitute code.
Fix: always commit go.sum. Remove it from .gitignore, run go mod tidy, and commit:
Bug 15 — Skipping go mod tidy after edits¶
$ go build
main.go:5:8: no required module provides package github.com/google/uuid;
to add it:
go get github.com/google/uuid
Bug: Editing source code does not automatically update go.mod. Many teams hit this in CI when one developer ran go run locally (which adds requires implicitly in some workflows) but never committed the result.
Fix: make go mod tidy part of your pre-commit and CI checks:
Bug 16 — Non-ASCII or whitespace in module path¶
or
Bug: The Go module system requires module paths to match a strict syntax: ASCII letters, digits, ., -, _, /, plus a few escapes. Spaces, accents, or emoji are rejected by the proxy, by go get, or — worse — accepted locally and broken remotely.
Fix: use plain ASCII, kebab- or snake-case:
Bug 17 — Vanity URL with a malformed meta tag¶
The team owns go.acme.dev/api and wants it to forward to GitHub. The HTML returned by https://go.acme.dev/api?go-get=1:
$ go get go.acme.dev/api
go: go.acme.dev/api: unrecognized import path "go.acme.dev/api":
parse https://go.acme.dev/api?go-get=1: meta tag missing VCS field
Bug: The go-import meta tag must contain three space-separated fields: <import-prefix> <vcs> <repo-url>. The VCS (git) is missing.
Fix:
Re-deploy and verify with curl:
Bug 18 — go.work accidentally committed¶
Bug: go.work is meant for local multi-module development. Once committed, every consumer who clones the repo gets workspace mode forced on, which silently overrides go.mod versions with whatever path the workspace points at — usually broken on their machine.
Fix: keep workspaces local. Add to .gitignore:
If you genuinely have a multi-module monorepo and want workspaces shared, document it explicitly and have CI verify the file is consistent — but the default is "do not commit."
Bug 19 — GOPRIVATE not set for internal modules¶
$ go get git.acme.internal/team/utils
go: git.acme.internal/team/utils@v1.2.0: reading
https://proxy.golang.org/git.acme.internal/team/utils/@v/v1.2.0.info:
410 Gone
verifying git.acme.internal/team/utils@v1.2.0: checksum database lookup
required for non-public module
Bug: By default Go fetches via proxy.golang.org and verifies via sum.golang.org. Both are public — they cannot see internal hosts. Without GOPRIVATE, every internal-only dep fails the checksum-DB lookup.
Fix: mark the internal namespace as private:
For finer control:
Commit a Makefile or tools.sh that sets these so new hires do not hit the same wall.
Bug 20 — Initialising the module in $HOME¶
$ pwd
/Users/me
$ go mod init github.com/me/myapp
go: creating new go.mod: module github.com/me/myapp
Bug: A go.mod now lives at $HOME. Every go build, go test, or go run issued from any subdirectory of $HOME thinks it is inside this module. Random scripts, dotfile experiments, and other repos behave bizarrely until the rogue go.mod is removed.
Fix: delete the home-level go.mod immediately:
Then create a real project directory and go mod init from there.
Bug 21 — Stale go.sum after a manual upgrade¶
$ go build
verifying github.com/spf13/cobra@v1.9.0: missing go.sum entry; for more
information, run 'go mod download github.com/spf13/cobra'
Bug: Someone hand-edited go.mod to bump cobra from v1.8.0 to v1.9.0 but did not regenerate go.sum. The toolchain now refuses to use a version it cannot verify.
Fix: never edit go.mod versions by hand for upgrades. Use go get:
This atomically updates go.mod and go.sum.
Bug 22 — Adding replace for a fork without pinning a commit¶
Bug: replace does not accept latest. It needs a real version, a pseudo-version, or a local path. The build either fails or, on older toolchains, silently picks something unexpected.
Fix: point at a real tag or commit:
For an unreleased commit, use the pseudo-version Go computes:
Bug 23 — Module path matches a typo-squatted package¶
Bug: Typo squatting is a real attack vector. sirrupsen is not sirupsen. The build "works" because something exists at that path — possibly a malicious mirror.
Fix: copy import paths from the official repo, never type them. Lock dependencies with go.sum, audit with go list -m all, and consider tools like govulncheck and osv-scanner.
Bug 24 — Forgetting to commit go.mod after go mod init¶
$ go mod init github.com/me/myapp
$ git add main.go
$ git commit -m "Initial commit"
$ git push
# CI:
$ go build
go: cannot find main module, but found .git/config in /workspace
to create a module there, run:
go mod init
Bug: go.mod was never staged. Locally everything works because the file exists; CI clones a repo without it and immediately fails.
Fix: include both files in the very first commit:
Add a CI step that fails fast if go.mod is missing:
Bug 25 — go mod init with a path that already has a release¶
Bug: Picking a module path you do not control is a recipe for disaster. The path collides with an existing module on the proxy. Anyone who imports your repo gets the other project's code from cache, and CI fails with "verifying ...: checksum mismatch" if they ever clear it.
Fix: module paths must be unique and owned by you. Use your own GitHub/GitLab namespace, or a vanity domain you control. If you are forking, append a suffix or use the /v2 major-version mechanism on your own path.
Summary¶
A working go.mod is the contract between your repo, your collaborators, the proxy, and every consumer that ever imports your code. Most module bugs come from one of three sins:
- Treating
go.modlike an editable text file. Usego mod edit,go get,go mod tidy— let the tooling keepgo.modandgo.sumconsistent. - Conflating local convenience with a published artifact.
replace,go.work, and home-directory experiments are local-only; never let them leak into a tag. - Skipping CI checks. A pipeline that runs
go mod tidyandgit diff --exit-codecatches almost every entry on this page before it reaches main.
Adopt those three habits and the rest of the module system becomes mostly invisible.