The 404 that stops your build
You write a clean import path. You run go get. The terminal spits out a 404 or a proxy timeout. You know the repository exists. You can clone it with git clone just fine. The Go toolchain just refuses to fetch it. This happens because Go assumes every module is public by default. It routes all dependency lookups through a centralized cache called the Go module proxy. Private repositories live outside that cache. The toolchain needs a direct signal to skip the proxy and talk to your Git server instead.
How the module proxy changes the rules
The Go module proxy (proxy.golang.org) is a read-only mirror of public packages. Think of it like a public library that keeps copies of every open-source book. When you request a dependency, the library hands you a cached copy instantly. It speeds up builds, guarantees reproducibility, and shields developers from upstream outages. When you run go get, the toolchain asks the proxy for the module first. If the proxy returns a 404, the build stops. It does not fall back to Git automatically. That design keeps the ecosystem secure and fast. It also means private code requires an explicit opt-out.
The GOPRIVATE environment variable is that opt-out. It accepts a comma-separated list of glob patterns. Any import path that matches a pattern bypasses the proxy entirely. The go command falls back to standard Git operations, using whatever credentials your terminal already has configured.
The minimal setup
Here is the simplest way to tell Go to skip the proxy for a specific organization.
# Tell the toolchain to bypass the proxy for everything under github.com/acme-corp
export GOPRIVATE="github.com/acme-corp/*"
# Fetch the private module directly from the Git remote
go get github.com/acme-corp/internal-utils@v1.2.0
The glob pattern * matches any path depth after the domain. Setting the variable in your shell session is enough for immediate use. The go command reads it before contacting any network endpoint.
Convention aside: Go does not store GOPRIVATE in go.mod. Environment variables stay outside version control by design. If your team needs consistent behavior, document the setup in your repository README or use a .envrc file with direnv.
The proxy is a cache, not a gatekeeper. GOPRIVATE is the key that unlocks direct Git access.
What happens under the hood
When you run go get, the toolchain follows a strict lookup chain. It first checks the local module cache at ~/go/pkg/mod. If the version is missing, it queries the module proxy using the Simple Module Protocol. The proxy responds with a go.mod file and a list of available versions. If GOPRIVATE matches the import path, the proxy step is skipped entirely. The go command runs git ls-remote against the repository URL to find tags, then checks out the requested version. It relies on your existing Git configuration for authentication. SSH keys, credential managers, or token-based HTTPS auth all work without extra Go configuration.
The toolchain also skips checksum verification for private modules by default. Public modules require a cryptographic hash in go.sum to prove the downloaded code matches the proxy. Private repositories often lack a public sum database, so Go assumes you trust your own Git server. You can override this behavior later if your organization runs a private sum database, but the default keeps private workflows frictionless.
Authentication lives in Git. Pattern matching lives in GOPRIVATE. Keep them separate and the build stays green.
Realistic workflow: team setup and persistence
Hardcoding export GOPRIVATE in every terminal session breaks after a reboot. Teams need a persistent, reproducible setup. Modern Go versions support writing environment variables directly to the toolchain configuration.
# Persist the private pattern across all terminal sessions
go env -w GOPRIVATE="github.com/acme-corp/*,gitlab.internal/*"
# Verify the toolchain picked up the pattern
go env GOPRIVATE
This writes the value to ~/.config/go/env on Linux and macOS, or the equivalent path on Windows. It survives shell restarts and works inside IDE terminals. You can combine multiple patterns with commas. The order does not matter. The toolchain evaluates all patterns against every import path during the fetch phase.
When you add a private dependency to a project, the go.mod file records the version requirement exactly like a public package. The only difference is the network path used during resolution.
module github.com/acme-corp/webapp
go 1.22
require (
// Public dependency fetched through the proxy
github.com/gin-gonic/gin v1.9.1
// Private dependency fetched directly from Git
github.com/acme-corp/internal-utils v1.2.0
)
The go.mod file contains no special markers for private modules. The environment variable handles the routing. This keeps go.mod portable and readable while keeping authentication concerns separate.
Keep go.mod clean. Route private traffic through environment variables, not file flags.
Checksums and the sum database
Public modules require a cryptographic hash in go.sum. The hash proves the downloaded code matches exactly what the proxy served. Private modules bypass this check by default. If you accidentally enable strict verification, the build will stall waiting for a hash that does not exist.
Go uses two separate variables to control checksum behavior. GONOSUMDB tells the toolchain which domains should not query the public sum database. GONOSUMCHECK tells the toolchain which domains should skip the go.sum verification entirely. The GOPRIVATE variable automatically implies both behaviors for matched patterns. You rarely need to set them manually. If your organization runs an internal sum database, you would set GOSUMDB to your internal endpoint and leave GOPRIVATE handling the routing.
The toolchain writes checksums for public dependencies into go.sum. It skips private dependencies unless you explicitly configure a sum database. This means go.sum often contains fewer lines than the number of packages in go.mod. That is expected behavior. Do not force checksums for internal code.
Trust the default checksum routing. Let Go skip verification for domains you control.
Pitfalls and error messages
Private module setups fail in predictable ways. The errors usually point to authentication or pattern mismatches.
If your Git credentials are missing or expired, the toolchain cannot reach the repository. You will see go: github.com/acme-corp/internal-utils: git ls-remote -q origin: exit status 128 in the terminal. That exit code means Git rejected the connection. Run git clone manually to verify your SSH keys or HTTPS tokens are working. Go does not manage credentials. It delegates entirely to Git.
Pattern mismatches are the second common failure. If you set GOPRIVATE="github.com/acme-corp" without the trailing /*, the toolchain only matches the exact string. It will not skip the proxy for github.com/acme-corp/internal-utils. The build fails with go: github.com/acme-corp/internal-utils: reading https://proxy.golang.org/github.com/acme-corp/internal-utils/... 404 Not Found. Add the wildcard to cover subpaths.
Checksum verification can also block private modules if you accidentally enable it. If your environment sets GONOSUMCHECK to an empty value or explicitly includes the private domain, Go will demand a hash in go.sum. Private repositories rarely publish sums. The compiler rejects the build with go: github.com/acme-corp/internal-utils@v1.2.0: missing go.sum entry for go.mod file; to add it: go mod download github.com/acme-corp/internal-utils. Clear GONOSUMCHECK or leave it unset. Go skips sum verification for GOPRIVATE matches by default.
Authentication lives in Git. Pattern matching lives in GOPRIVATE. Keep them separate and the build stays green.
When to use what
Private dependencies require different routing strategies depending on your infrastructure. Pick the right tool for the network boundary.
Use GOPRIVATE when your team hosts code on standard Git providers like GitHub, GitLab, or Bitbucket and needs direct access. Use a replace directive in go.mod when you are developing a private module locally and want to override the version with a filesystem path. Use a corporate module proxy when your organization requires strict audit logging, internal caching, and centralized access control. Use public modules without any environment variables when the code is open source and available on proxy.golang.org.
Route by network boundary, not by file structure.