By Paul Jolly, Go contributor, and co-creator of play-with-go.dev.

The go command defaults to downloading modules from the public Go module mirror at proxy.golang.org. It also defaults to validating downloaded modules, regardless of source, against the public Go checksum database at sum.golang.org. These defaults work well for publicly available source code.

But what happens if you and your fellow developers need to work with private modules?

This guide explains how to work with private modules. In the guide you will create three modules:

  • {{{.PUBLIC}}}, a publicly accessible module that provides a Message() function
  • {{{.PRIVATE}}}, a private module that provides a Secret() function
  • gopher, a local-only module that uses public and private modules

Prerequisites

You should already have completed:

This guide is running using:

$ go version
go version go1.19.1 linux/amd64

The public and private modules

Start by initialising your public module:

$ mkdir /home/gopher/public
$ cd /home/gopher/public
$ go mod init {{{.PUBLIC}}}
go: creating new go.mod: module {{{.PUBLIC}}}
$ git init -q
$ git remote add origin https://{{{.PUBLIC}}}.git

Create an initial version of the Message() in public.go:

package public

func Message() string {
	return "This is a public safety announcement!"
}

Commit and push this initial version:

$ git add public.go go.mod
$ git commit -q -m 'Initial commit of public module'
$ git push -q origin main
remote: . Processing 1 references        
remote: Processed 1 references in total        

Now do the same for the private module:

$ mkdir /home/gopher/private
$ cd /home/gopher/private
$ go mod init {{{.PRIVATE}}}
go: creating new go.mod: module {{{.PRIVATE}}}
$ git init -q
$ git remote add origin https://{{{.PRIVATE}}}.git

Note: the private source code repository at https://{{{.PRIVATE}}}.git was automatically created for you when this guide loaded, much like https://{{{.PUBLIC}}}.git was created for the public module. However, the private module repository was marked as Private: true, hence authenticated access is required to access https://{{{.PRIVATE}}}.git.

Create an initial version of the Secret() in private.go:

package private

func Secret() string {
	return "This is a top secret message... for your eyes only"
}

Commit and push this initial version:

$ git add private.go go.mod
$ git commit -q -m 'Initial commit of private module'
$ git push -q origin main
remote: . Processing 1 references        
remote: Processed 1 references in total        

The gopher module

Now create a gopher module to try out the public and private modules. Unlike the public and private modules, you will not publish the gopher module; it will be local only:

$ mkdir /home/gopher/gopher
$ cd /home/gopher/gopher
$ go mod init gopher
go: creating new go.mod: module gopher

Create an initial version of a main package that uses the two modules, in gopher.go:

package main

import (
	"fmt"

	"{{{.PUBLIC}}}"
	"{{{.PRIVATE}}}"
)

func main() {
	fmt.Printf("public.Message(): %v\n", public.Message())
	fmt.Printf("private.Secret(): %v\n", private.Secret())
}

At this point, let’s take a small diversion to talk about proxies.

Module proxies

The go command can fetch modules from a proxy or connect to source control servers directly, according to the setting of the GOPROXY environment variable (see go help env). You can see the default setting for GOPROXY by inspecting the output of go env:

$ go env GOPROXY
https://proxy.golang.org,direct

This means it will try the Go module mirror run by Google and fall back to a direct connection if the proxy reports that it does not have the module (HTTP error 404 or 410).

The go command also defaults to validating downloaded modules, regardless of source, against the public Go checksum database at sum.golang.org, something that is controlled by the GOSUMDB environment variable. You can see the default for GOSUMDB by checking the output of go env:

$ go env GOSUMDB
sum.golang.org

Because your session is already configured with authentication credentials for the source control system that hosts {{{.PRIVATE}}}, attempting to go get that module will succeed because the go command will fall back to the direct mode.

Let’s simulate getting our module dependencies with no credentials by setting GOPROXY to only use the public proxy, using the go env command:

$ go env -w GOPROXY=https://proxy.golang.org

Add a dependency on the public module:

$ go get {{{.PUBLIC}}}
go: downloading {{{.PUBLIC}}} v0.0.0-20060102150405-abcedf12345
go: added {{{.PUBLIC}}} v0.0.0-20060102150405-abcedf12345

As expected, that succeeded.

Try to add a dependency on the private module:

$ go get {{{.PRIVATE}}}
go: module {{{.PRIVATE}}}: reading https://proxy.golang.org/{{{.PRIVATE}}}/@v/list: 404 Not Found
	server response:
	not found: module {{{.PRIVATE}}}: git ls-remote -q origin in /tmp/gopath/pkg/mod/cache/vcs/6f24e6c3f0f418f23971c3f3f96e9d70729d1ef5d14781310442dcb8d214d2f6: exit status 128:
		fatal: could not read Username for 'https://gopher.live': terminal prompts disabled
	Confirm the import path was entered correctly.
	If this is a private repository, see https://golang.org/doc/faq#git_https for additional information.

Thankfully, this failed.

Let’s return GOPROXY to its default value:

$ go env -w GOPROXY=

And try once again to add a dependency on the private module:

$ go get {{{.PRIVATE}}}
go: downloading {{{.PRIVATE}}} v0.0.0-20060102150405-abcedf12345
go: {{{.PRIVATE}}}@v0.0.0-20060102150405-abcedf12345: verifying module: {{{.PRIVATE}}}@v0.0.0-20060102150405-abcedf12345: reading https://sum.golang.org/lookup/{{{.PRIVATE}}}@v0.0.0-20060102150405-abcedf12345: 404 Not Found
	server response:
	not found: {{{.PRIVATE}}}@v0.0.0-20060102150405-abcedf12345: invalid version: git ls-remote -q origin in /tmp/gopath/pkg/mod/cache/vcs/6f24e6c3f0f418f23971c3f3f96e9d70729d1ef5d14781310442dcb8d214d2f6: exit status 128:
		fatal: could not read Username for 'https://gopher.live': terminal prompts disabled
	Confirm the import path was entered correctly.
	If this is a private repository, see https://golang.org/doc/faq#git_https for additional information.

This fails because the checksum database is not able to access your private module. But it’s worse than that, because the go command “leaked” a request for {{{.PRIVATE}}} to the public proxy. This might well be fine for a trusted proxy like the Google proxy, but it isn’t always the case.

The GOPRIVATE environment variable

The GOPRIVATE environment variable controls which modules the go command considers to be private (not available publicly) and should therefore not use the proxy or checksum database. The variable is a comma-separated list of glob patterns (in the syntax of Go’s path.Match) of module path prefixes.

Let’s tell the go command that {{{.PRIVATE}}} by setting the GOPRIVATE environment variable:

$ go env -w GOPRIVATE={{{.PRIVATE}}}

Try to get the latest version of the private module again (remember, the public module succeeded):

$ go get {{{.PRIVATE}}}
go: downloading {{{.PRIVATE}}} v0.0.0-20060102150405-abcedf12345
go: added {{{.PRIVATE}}} v0.0.0-20060102150405-abcedf12345

Success! As a final check, run the gopher module main package:

$ go run .
public.Message(): This is a public safety announcement!
private.Secret(): This is a top secret message... for your eyes only

For more details on the GOPRIVATE environment variable and the values it can take, see go help module-auth, which also includes examples of how to use the * glob to match multiple sub domains or modules.

The GONOPROXY and GONOSUMDB environment variables can be used for more fine grained control. Again, see go help module-auth for more information.

Conclusion

This guide has provided you with a brief introduction to handling private modules.

As a next step you might like to consider: