# Go - Modules, Workspaces, and GOPROXY
In a `go.work` monorepo, plain go commands operate on the merged workspace, so per-module work needs `GOWORK=off` and bumps need `go work sync`. The proxy and checksum database make published versions immutable and version resolution surprising. Standard-library membership follows one rule: the first path element has no dot.
## Key points
- Plain `go test` in a `go.work` workspace tests the merged workspace. Match per-module CI with `GOWORK=off go test ./...`.
- Verify refactors in both modes before you call them green. Workspace-mode `go vet` compiles every test file in the module.
- Run nested tools module commands with `GOWORK=off` when a `go.work` exists. Workspace setup drops those modules.
- A root `go.mod` bump alone does not update a workspace, because MVS selects the highest minimum across all `use` directives, so run `GOWORK=off go get`, `GOWORK=off go mod tidy`, then `go work sync`.
- After a partial member upgrade, re-tidy every module and regenerate a complete `go.work.sum`. A partial upgrade can leave only `/go.mod` hashes and no content hashes, so MVS selects a release nothing vouches for. This differs from a stale `go.sum`.
- Use one combined `go get` per module. Sequential calls re-resolve the graph, oscillate versions, and can silently bump the `go` directive.
- Nested modules need no `go.work` entries for CI. Discover with `find . -mindepth 2 -type f -name go.mod` and run `go test ./...` in each.
- Find modules needing a version bump with `find . -name go.mod | xargs grep -l '^go '`.
- Scope scans such as `govulncheck` per module or workspace mode to avoid unrelated modules.
- The `internal/` rule is lexical and crosses module boundaries in one repository: a module rooted at `internal` above the version suffix is importable by every in-repo module, denied externally, and usable as a shared core by companion modules.
- Put repository-wide test helpers in a root-module package, not under a nested `internal/`. Modules outside that subtree cannot import them.
- Once a tag's hash lands in sum.golang.org, retagging is unrecoverable: the append-only log and proxy cache keep `go get` serving the old code. Check sum.golang.org, a read-only lookup, before tag surgery, because touching proxy.golang.org registers and caches the version.
- A `retract` directive takes effect only from a later version's `go.mod`. The toolchain warns dependents and skips the version. The release page still needs its own notice.
- A cached pre-publication 404 expires on its own. Use `go list -m <module>@<version>` to confirm a tag exists.
- A commit pseudo-version `v1.0.0-dev.0.<ts>-<sha>` sorts below an existing `v1.0.0-dev.1` tag, so `go get` refuses the downgrade and `go mod tidy` silently falls back to an older release, a vacuous green. Pin the commit with a `replace` directive and a fallback-rejecting assertion.
- A module's `replace` directives do not apply to consumers. A throwaway consumer resolves the published tag, so a new root-module package breaks proxy-resolution checks until the next tag.
- `GOPROXY` comma syntax falls through only on 404 and 410. A stream error still fails the build. The pipe form `https://proxy.golang.org|direct` falls back to a direct VCS fetch on any error.
- Transient `proxy.golang.org` and `sum.golang.org` `INTERNAL_ERROR` errors in CI are flakes. Rerun the failed jobs.
- A Dockerfile pre-download with `go mod download` suffices for a fully offline vendor step plus build, proven from fresh caches with the checksum database enabled. Pass the pipe `GOPROXY` as a build arg.
- `GOPROXY=direct go list -m -json <module>@<commit>` resolves any commit, not only tags, and yields a pseudo-version for VEX identifiers.
- Guard versions written into `go.mod` with `semver.IsValid`, not an exclusion list. A from-source build reports `Version = "(devel)"`, invalid in a `require` line, so the next parse fails.
- A major-version module may live in a subdirectory, tagged with the prefix such as `sub/v2.0.0`. A real v2 module and an alias shim at the old import path can coexist, so user sources keep the old path.
- `go install <pkg>/cmd/...@latest` matches only `main` packages, provided all matches live in one module at one version. Thin `cmd/` entrypoints in one root module yield a single-command install.
- Standard-library detection is "the first import path element has no dot", the rule `go/build.IsStandardImportPath` uses. A static prefix list drifts. The rule also works on runtime frames.
## How to apply
1. Bump a shared dependency: `GOWORK=off go get <module>@<version> && GOWORK=off go mod tidy && go work sync`.
2. Set `GOPROXY='https://proxy.golang.org|direct'` and pass it as a Docker build arg.
3. Pin a commit under test with a `replace` directive and assert the resolved version.
## Observations
- [go] Plain `go test` in a workspace tests the merged modules, so single-module verification needs `GOWORK=off`. #go (source: agent sessions, 2026-09)
- [go] A commit pseudo-version sorts below an existing dev tag, so `go mod tidy` can fall back to an older release. #go (source: agent sessions, 2026-09)
- [ci] Proxy and checksum-database `INTERNAL_ERROR` stream errors are transient flakes, so rerun failed CI jobs. #ci (source: agent sessions, 2026-10)
## Relations
- relates_to [[resources/documentation/Go/Go - Toolchain Pinning and the Build Cache]] (`go` directive bumps from sequential `go get`)
- relates_to [[resources/documentation/Go/Go - Testing Patterns and Tests That Certify Nothing]] (vacuous greens from version fallback)
- relates_to [[resources/documentation/Go/Go - Lint and Vulnerability Tooling]] (`govulncheck` scoping)
- relates_to [[resources/documentation/Go/Go - Internals]] (stdlib detection of runtime frames)
## Sources
- Personal agent-session notes, September 2025 to October 2026