In version 1.13, the authors of Go added a new way of managing the libraries a Go project depends on, called Go modules. Go modules were added in response to a growing need to make it easier for developers to maintain various versions of their dependencies, as well as add more flexibility in the way developers organize their projects on their computer. Go modules commonly consist of one project or library and contain a collection of Go packages that are then released together. Go modules solve many problems with GOPATH, the original system, by allowing users to put their project code in their chosen directory and specify versions of dependencies for each module.
Although Go modules were introduced in Go 1.11 and became the default in later versions (Go 1.16+), they are now the standard dependency management system in modern Go. The GOPATH workflow is no longer recommended for new projects.
In this tutorial, you will create your own public Go module and add a package to your new module. You will also add remote dependencies to your project and learn how to reference specific module versions using tags, branches, and commits. Additionally, you’ll explore advanced workflows including Go workspaces for managing multiple modules, the replace directive for local development, and how to configure access to private modules. You’ll also learn how the behavior of go get has evolved across Go versions to provide clearer dependency management.
Key Takeaways:
GOPATH. The go.mod file defines your module and its dependencies, while go.sum contains cryptographic checksums that ensure security and reproducibility.go.mod and go.sum to version control to ensure consistent builds across different environments and prevent dependency tampering.go mod tidy regularly after adding or removing imports, running go get, or before committing changes. This command removes unused dependencies and adds missing ones automatically.go get to manage module dependencies in your project and go install to build and install executable binaries. This separation provides clearer intent and prevents accidentally modifying go.mod.@latest, @v1.2.3, @branch-name, or @commit-hash when adding dependencies. This ensures reproducible builds and makes your dependency requirements clear.go.mod files. Create a workspace with go work init and add modules with go work use, keeping the workspace file local to your development environment.GOPRIVATE environment variable for private repositories to prevent Go from sending module paths to public proxies. Combine this with SSH keys or personal access tokens for authentication.// indirect in go.mod are transitive dependencies—modules required by your direct dependencies but not imported directly in your code. Go tracks these to ensure complete build reproducibility.replace directive in go.mod for permanent module replacements like redirecting to a fork or using a local version in a monorepo. Unlike workspaces, replace directives are committed and shared with your team.@ syntax. This gives you fine-grained control over dependency versions, from stable releases to specific commits.To follow this tutorial, you will need:
Go version 1.18 or greater installed. To set this up, follow the How To Install Go tutorial for your operating system.
Familiarity with writing packages in Go. To learn more, follow the How To Write Packages in Go tutorial.
(Optional) Basic familiarity with Git and version control systems. Since Go modules are typically distributed via repositories, understanding how repositories and version tags work will be helpful.
At first glance, a Go module looks similar to a Go package. A module has a number of Go code files implementing the functionality of a package, but it also has two additional and important files in the root: the go.mod file and the go.sum file. These files contain information the go tool uses to keep track of your module’s configuration, and are commonly maintained by the tool so you don’t need to.
The first thing to do is decide the directory the module will live in. With the introduction of Go modules, it became possible for Go projects to be located anywhere on the filesystem, not just a specific directory defined by Go. You may already have a directory for your projects, but in this tutorial, you’ll create a directory called projects and the new module will be called mymodule. You can create the projects directory either through an IDE or via the command line.
Note: In real-world projects, module names are typically based on a repository path (for example, github.com/username/mymodule) so they can be imported by others. For simplicity, this tutorial uses mymodule.
If you’re using the command line, begin by making the projects directory and navigating to it:
- mkdir projects
- cd projects
Next, you’ll create the module directory itself. Usually, the module’s top-level directory name is the same as the module name, which makes things easier to keep track of. In your projects directory, run the following command to create the mymodule directory:
- mkdir mymodule
Once you’ve created the module directory, the directory structure will look like this:
└── projects
└── mymodule
The next step is to create a go.mod file within the mymodule directory to define the Go module itself. To do this, you’ll use the go tool’s mod init command and provide it with the module’s name, which in this case is mymodule. Now create the module by running go mod init from the mymodule directory and provide it with the module’s name, mymodule:
- go mod init mymodule
Note: In production scenarios, you would typically run:
- go mod init github.com/username/mymodule
Using a fully qualified module path ensures your module can be imported and versioned correctly.
This command will return the following output when creating the module:
Outputgo: creating new go.mod: module mymodule
With the module created, your directory structure will now look like this:
└── projects
└── mymodule
└── go.mod
Now that you have created a module, let’s take a look inside the go.mod file to see what the go mod init command did.
go.mod FileWhen you run commands with the go tool, the go.mod file is a very important part of the process. It’s the file that contains the name of the module and versions of other modules your own module depends on. It can also contain other directives, such as replace, which can be helpful for doing development on multiple modules at once.
In the mymodule directory, open the go.mod file using nano, or your favorite text editor:
- nano go.mod
The contents will look similar to this, which isn’t much:
module mymodule
go 1.26
The first line, the module directive, tells Go the name of your module so that when it’s looking at import paths in a package, it knows not to look elsewhere for mymodule. The mymodule value comes from the parameter you passed to go mod init:
module mymodule
The only other line in the file at this point, the go directive, tells Go which version of the language the module is targeting. In this case, since the module was created using a recent version of Go, the go directive reflects that version:
go 1.26
Note: The go directive specifies the minimum Go version required for the module and can affect module behavior. In modern Go versions, this value is automatically updated by the go tool when needed.
As more information is added to the module, this file will expand, but it’s a good idea to look at it now to see how it changes as dependencies are added further on.
In addition to go.mod, you will also see a go.sum file created as dependencies are added. The go.sum file stores cryptographic checksums of your module’s dependencies to ensure their integrity. This helps guarantee that the same dependency versions are used across different environments and that the downloaded modules have not been tampered with.
Note: You should commit both go.mod and go.sum to version control to ensure reproducible builds.
You’ve now created a Go module with go mod init and looked at what an initial go.mod file contains, but your module doesn’t do anything yet. It’s time to take your module further and add some code.
To ensure the module is created correctly and to add code so you can run your first Go module, you’ll create a main.go file within the mymodule directory. The main.go file is commonly used in Go programs to signal the starting point of a program. The file’s name isn’t as important as the main function inside, but matching the two makes it easier to find. In this tutorial, the main function will print out Hello, Modules! when run.
To create the file, open the main.go file using nano, or your favorite text editor:
- nano main.go
In the main.go file, add the following code to define your main package, import the fmt package, then print out the Hello, Modules! message in the main function:
package main
import "fmt"
func main() {
fmt.Println("Hello, Modules!")
}
In Go, each directory is considered its own package, and each file has its own package declaration line. In the main.go file you just created, the package is named main. Typically, you can name the package any way you’d like, but the main package is special in Go. When Go sees that a package is named main, it knows the package should be considered a binary, and should be compiled into an executable file instead of a library designed to be used in another program.
After the package is defined, the import declaration says to import the fmt package so you can use its Println function to print the "Hello, Modules!" message to the screen.
Finally, the main function is defined. The main function is another special case in Go, related to the main package. When Go sees a function named main inside a package named main, it knows the main function is the first function it should run. This is known as a program’s entry point.
Once you have created the main.go file, the module’s directory structure will look similar to this:
└── projects
└── mymodule
└── go.mod
└── main.go
If you are familiar with using Go and the GOPATH, running code in a module is similar to how you would do it from a directory in the GOPATH. (Don’t worry if you are not familiar with the GOPATH, because using modules replaces its usage.)
There are two common ways to run an executable program in Go: building a binary with go build or running a file with go run. In this tutorial, you’ll use go run to run the module directly instead of building a binary, which would have to be run separately.
Run the main.go file you’ve created with go run:
- go run main.go
Running the command will print the Hello, Modules! text as defined in the code:
OutputHello, Modules!
Tip: In module-aware mode (default in modern Go versions), you can also run the entire module using:
- go run .
In this section, you added a main.go file to your module with an initial main function that prints Hello, Modules!. At this point, your program doesn’t yet benefit from being a Go module — it could be a file anywhere on your computer being run with go run. The first real benefit of Go modules is being able to add dependencies to your project in any directory and not just the GOPATH directory structure. You can also add packages to your module. In the next section, you will expand your module by creating an additional package within it.
Similar to a standard Go package, a module may contain any number of packages and sub-packages, or it may contain none at all. For this example, you’ll create a package named mypackage inside the mymodule directory.
Create this new package by running the mkdir command inside the mymodule directory with the mypackage argument:
- mkdir mypackage
This will create the new directory mypackage as a sub-package of the mymodule directory:
└── projects
└── mymodule
└── mypackage
└── main.go
└── go.mod
Use the cd command to change the directory to your new mypackage directory, and then use nano, or your favorite text editor, to create a mypackage.go file. This file could have any name, but using the same name as the package makes it easier to find the primary file for the package:
- cd mypackage
- nano mypackage.go
In the mypackage.go file, add a function called PrintHello that will print the message Hello, Modules! This is mypackage speaking! when called:
package mypackage
import "fmt"
func PrintHello() {
fmt.Println("Hello, Modules! This is mypackage speaking!")
}
Since you want the PrintHello function to be available from another package, the capital P in the function name is important. The capital letter means the function is exported and available to any outside program. For more information about how package visibility works in Go, Understanding Package Visibility in Go includes more detail.
Now that you’ve created the mypackage package with an exported function, you will need to import it from the mymodule package to use it. This is similar to how you would import other packages, such as the fmt package previously, except this time you’ll include your module’s name at the beginning of the import path. Open your main.go file from the mymodule directory and add a call to PrintHello by adding the highlighted lines below:
package main
import (
"fmt"
"mymodule/mypackage"
)
func main() {
fmt.Println("Hello, Modules!")
mypackage.PrintHello()
}
If you take a closer look at the import statement, you’ll see the new import begins with mymodule, which is the same module name you set in the go.mod file. This is followed by the path separator and the package you want to import, mypackage in this case:
"mymodule/mypackage"
<$>[note]
Note: In real-world projects where the module path is a repository URL (for example, github.com/username/mymodule), the import path would include that full path:
"github.com/username/mymodule/mypackage"
In the future, if you add packages inside mypackage, you would also add them to the end of the import path in a similar way. For example, if you had another package called extrapackage inside mypackage, your import path for that package would be mymodule/mypackage/extrapackage.
Run your updated module with go run from the mymodule directory as before:
- go run .
When you run the module again you’ll see both the Hello, Modules! message from earlier as well as the new message printed from your new mypackage’s PrintHello function:
OutputHello, Modules!
Hello, Modules! This is mypackage speaking!
You’ve now added a new package to your initial module by creating a directory called mypackage with a PrintHello function. As your module’s functionality expands, though, it can be useful to start using other peoples’ modules in your own. In the next section, you’ll add a remote module as a dependency to yours.
Go modules are distributed from version control repositories, commonly Git repositories. When you want to add a new module as a dependency to your own, you use the repository’s path as a way to reference the module you’d like to use. When Go sees the import path for these modules, it can infer where to find it remotely based on this repository path.
For this example, you’ll add a dependency on the Cobra library to your module. Cobra is a popular library for creating console applications, but we won’t address that in this tutorial.
Similar to when you created the mymodule module, you’ll again use the go tool. However, this time, you’ll run the go get command from the mymodule directory. In modern versions of Go, go get is used to add or update dependencies in your go.mod file, while installing executables is handled separately using go install.
Run go get and provide the module you’d like to add. In this case, you’ll get github.com/spf13/cobra:
- go get github.com/spf13/cobra@latest
When you run this command, the go tool will look up the Cobra repository from the path you specified and determine which version of Cobra is the latest by looking at the repository’s branches and tags. It will then download that version and keep track of the one it chose by adding the module name and the version to the go.mod file for future reference.
After adding a dependency, it’s recommended to run:
- go mod tidy
This command cleans up unused dependencies and ensures that go.mod and go.sum are in sync.
Now, open the go.mod file in the mymodule directory to see how the go tool updated the go.mod file when you added the new dependency. The example below could change depending on the current version of Cobra that’s been released or the version of the Go tooling you’re using, but the overall structure of the changes should be similar:
module mymodule
go 1.26
require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/spf13/cobra v1.7.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)
A new section using the require directive has been added. This directive tells Go which module you want, such as github.com/spf13/cobra, and the version of the module you added. Sometimes require directives will also include an // indirect comment. This comment says that, at the time the require directive was added, the module is not referenced directly in any of the module’s source files. A few additional require lines were also added to the file. These lines are other modules Cobra depends on that the Go tool determined should be referenced as well.
You may have also noticed a new file, go.sum, was created in the mymodule directory after adding the dependency. This file contains checksums for downloaded modules and ensures that dependencies remain consistent and secure across different environments.
Once you have the dependency downloaded you’ll want to update your main.go file with some minimal Cobra code to use the new dependency. Update your main.go file in the mymodule directory with the Cobra code below to use the new dependency:
package main
import (
"fmt"
"github.com/spf13/cobra"
"mymodule/mypackage"
)
func main() {
cmd := &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hello, Modules!")
mypackage.PrintHello()
},
}
fmt.Println("Calling cmd.Execute()!")
cmd.Execute()
}
This code creates a cobra.Command structure with a Run function containing your existing “Hello” statements, which will then be executed with a call to cmd.Execute(). Now, run the updated code:
- go run .
You’ll see the following output, which looks similar to what you saw before. This time, though, it’s using your new dependency as shown by the Calling cmd.Execute()! line:
OutputCalling cmd.Execute()!
Hello, Modules!
Hello, Modules! This is mypackage speaking!
Using go get to add or update a remote dependency, such as github.com/spf13/cobra, makes it easier to keep your dependencies aligned with the versions defined in your module. However, in many cases, you may want more control over which version of a module is used in your project. For example, you might want to use a specific release version, a development branch, or even a particular commit.
In the next section, you’ll use go get with version identifiers to reference these different types of module versions.
Since Go modules are distributed from a version control repository, they can use version control features such as tags, branches, and even commits. You can reference these in your dependencies using the @ symbol at the end of the module path along with the version you’d like to use. Earlier, when you installed the latest version of Cobra, you were taking advantage of this capability, but you didn’t need to add it explicitly to your command. The go tool knows that if a specific version isn’t provided using @, it should use the special version latest.
For example, when you added your dependency initially, you could have also used the following command for the same result:
- go get github.com/spf13/cobra@latest
In modern Go workflows, specifying a version explicitly using @ is recommended to ensure clarity and reproducibility of your builds.
Now, imagine there’s a module you use that’s currently in development. For this example, call it your_domain/sammy/awesome. There’s a new feature being added to this awesome module and work is being done in a branch called new-feature. To add this branch as a dependency of your own module you would provide go get with the module path, followed by the @ symbol, followed by the name of the branch:
- go get your_domain/sammy/awesome@new-feature
Running this command would cause go to connect to the your_domain/sammy/awesome repository, download the new-feature branch at the current latest commit for the branch, and add that information to the go.mod file.
Branches aren’t the only way you can use the @ option, though. This syntax can be used for tags and even specific commits to the repository. For example, sometimes the latest version of the library you’re using may have a broken commit. In these cases, it can be useful to reference the commit just before the broken commit.
Using your module’s Cobra dependency as an example, suppose you need to reference commit 07445ea of github.com/spf13/cobra because it has some changes you need and you can’t use another version for some reason. In this case, you can provide the commit hash after the @ symbol the same as you would for a branch or a tag. Run the go get command in your mymodule directory with the module and version to download the new version:
- go get github.com/spf13/cobra@07445ea
If you open your module’s go.mod file again you’ll see that go get has updated the require line for github.com/spf13/cobra to reference the commit you specified:
module mymodule
go 1.26
require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/spf13/cobra v1.1.2-0.20210209210842-07445ea179fc // indirect
github.com/spf13/pflag v1.0.5 // indirect
)
Since a commit is a particular point in time, unlike a tag or a branch, Go includes additional information in the require directive to ensure it’s using the correct version in the future. If you look closely at the version, you’ll see it does include the commit hash you provided: v1.1.2-0.20210209210842-07445ea179fc.
Go modules also use this functionality to support releasing different versions of the module. When a Go module releases a new version, a new tag is added to the repository with the version number as the tag. If you want to use a specific version, you can look at a list of tags in the repository to find the version you’re looking for. If you already know the version, you may not need to search through the tags because version tags are named consistently.
Returning to Cobra as an example, suppose you want to use Cobra version 1.1.1. You could look at the Cobra repository and see it has a tag named v1.1.1, among others. To use this tagged version, you would use the @ symbol in a go get command, just as you would use a non-version tag or branch. Now, update your module to use Cobra 1.1.1 by running the go get command with v1.1.1 as the version:
- go get github.com/spf13/cobra@v1.1.1
Now if you open your module’s go.mod file, you’ll see go get has updated the require line for github.com/spf13/cobra to reference the version you provided:
module mymodule
go 1.26
require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/spf13/cobra v1.1.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)
Finally, if you’re using a specific version of a library, such as the 07445ea commit or v1.1.1 from earlier, but you determine you’d rather start using the latest version, it’s possible to do this by using the special latest version. To update your module to the latest version of Cobra, run go get again with the module path and the latest version:
- go get github.com/spf13/cobra@latest
Once this command finishes, the go.mod file will update to reflect the latest available version of the module.
The go get command is a powerful tool you can use to manage dependencies in your go.mod file without needing to edit it manually. As you saw in this section, using the @ character with a module name allows you to use particular versions for a module, from release versions to specific repository commits. It can even be used to go back to the latest version of your dependencies. Using a combination of these options will allow you to ensure the stability and reproducibility of your programs.
In modern Go development, it’s common to work with multiple related modules at the same time. For example, you might have one module for a library and another for an application that depends on it. Traditionally, managing such setups required using the replace directive or publishing intermediate versions of modules.
Starting with Go 1.18, the Go toolchain introduced workspaces using the go work command. Workspaces allow you to group multiple modules together and work on them simultaneously without modifying their individual go.mod files.
To create a workspace, navigate to a directory that will contain your related modules and run:
- go work init
This will create a go.work file in the current directory.
Next, add the modules you want to include in the workspace. For example, if you have two modules named mymodule and mylibrary, you can add them like this:
- go work use ./mymodule ./mylibrary
This command updates the go.work file to include both modules.
The resulting go.work file will look similar to this:
go 1.26
use (
./mymodule
./mylibrary
)
When you run Go commands (such as go build, go run, or go test) inside a workspace, the Go toolchain will resolve dependencies using the local modules listed in the go.work file instead of downloading them from remote repositories.
This makes it easier to:
replace directives during developmentWorkspaces are particularly useful in the following scenarios:
replaceBefore workspaces were introduced, developers commonly used the replace directive in go.mod to point to local module paths. While replace is still useful, workspaces provide a cleaner and more scalable solution for multi-module development without modifying module definitions.
In the next section, you’ll learn more about the replace directive and how it is still used in specific scenarios.
replace DirectiveThe replace directive in the go.mod file allows you to override where a module is resolved from. This is especially useful during development when you want to use a local version of a module or test changes without publishing a new version.
While Go workspaces (go work) are now the preferred way to work with multiple modules locally, the replace directive is still widely used in several important scenarios.
The replace directive maps one module path to another location:
replace <module-path> => <replacement-path>
The replacement can be:
Suppose your module depends on github.com/username/mylibrary, but you have a local copy of that library you want to test.
Add the following to your go.mod file:
replace github.com/username/mylibrary => ../mylibrary
Now, instead of downloading the module from the remote repository, Go will use the local directory ../mylibrary.
This is useful when:
Sometimes you may need to use a fork of a dependency—for example, to apply a temporary fix or experiment with changes.
replace github.com/original/library => github.com/yourfork/library v1.2.3
This tells Go to use your forked version instead of the original module.
In monorepo setups, you may have multiple modules in the same repository. You can use replace to point to a local module path:
replace github.com/username/project/mylibrary => ./mylibrary
This ensures that local changes are used without needing to publish intermediate versions.
replace directive only affects your local module. It is not applied transitively to other modules that depend on your code.replace directives are often used for development purposes only and may be removed before publishing a module.replace directive that points to a local path, other developers may encounter errors if the path does not exist on their system.In the next section, you’ll explore how the behavior of go get has changed across Go versions, which is essential for understanding how dependency management works in modern Go.
go get Behavior Across Go VersionsThe behavior of the go get command has evolved significantly across different Go versions, which has caused confusion for developers working with projects that span multiple Go versions or following tutorials written for older Go releases. Understanding these changes is essential for managing dependencies correctly and avoiding common pitfalls in modern Go development.
In Go 1.15 and earlier versions, go get served multiple purposes simultaneously. A single command could download dependencies, update your module files, and install executable binaries—all in one operation. This design led to ambiguous behavior that often confused developers.
For example, running this command:
- go get github.com/spf13/cobra
Would perform several actions at once:
cobra package and its dependenciesgo.mod file (if inside a module)cobra binary into your $GOPATH/bin directory (if the package contained an executable)This overloaded functionality created problems in practice. For instance, if you wanted to use cobra as a library dependency in your project, you might inadvertently install its binary as well. Conversely, if you only wanted to install a CLI tool, you would still modify your project’s dependencies. This dual behavior made it difficult to express clear intent and led to bloated go.mod files containing dependencies that weren’t actually needed by the project code.
Additionally, in automated environments like continuous integration pipelines, the lack of explicit version control meant that go get could pull in different versions depending on when it was run, leading to non-reproducible builds.
Starting with Go 1.16, the Go team began addressing these issues by introducing a clearer separation of concerns. This transition was completed in Go 1.17, which established two distinct commands with specific responsibilities:
go get: Exclusively manages dependencies in your go.mod file (adding, updating, or downgrading versions)go install: Exclusively builds and installs executable binaries to your $GOBIN or $GOPATH/bin directoryThis separation clarified the intent of each operation. If you wanted to install a CLI tool like cobra, you would now use:
- go install github.com/spf13/cobra-cli@latest
This command installs the executable without touching your project’s go.mod file. Conversely, if you wanted to add cobra as a library dependency for your project, you would use:
- go get github.com/spf13/cobra@latest
This change immediately improved several aspects of Go development:
@version syntax became standard practice, ensuring consistent dependency resolutiongo.mod, not tools you happen to have installedDuring this transition period, the Go toolchain displayed deprecation warnings to help developers adjust their workflows.
In Go 1.21 and subsequent versions, the behavior introduced during the transition period became fully standardized. The distinction between go get and go install is now strictly enforced, and best practices have crystallized around explicit version specification.
The modern workflow for managing dependencies follows this pattern:
- go get github.com/spf13/cobra@latest
- go mod tidy
The first command adds or updates the dependency in your go.mod file. The second command (go mod tidy) performs important cleanup operations:
go.sum file with cryptographic checksums for security and reproducibilityWhen you run go get, the Go toolchain intelligently updates your go.mod file by analyzing your import statements and the dependency graph. It determines the appropriate version based on semantic versioning rules and compatibility requirements, then records this information in your module files.
Version specifications are now explicit and required in most cases:
@latest: Use the most recent tagged release version@v1.2.3: Use a specific semantic version@main or @master: Use the latest commit from a specific branch@commit-hash: Use a specific commit (useful for unreleased patches)These changes represent a significant improvement in Go’s dependency management system, making builds more reliable, reproducible, and easier to reason about. By understanding the evolution of go get, you can avoid common pitfalls and work confidently with Go modules across different projects and Go versions.
In the next section, you’ll learn how to work with private modules using environment variables such as GOPROXY and GOPRIVATE.
In many real-world development scenarios, especially in corporate and enterprise environments, you’ll need to work with private Go modules that aren’t publicly accessible. These might be proprietary libraries hosted on private GitHub or GitLab repositories, internal packages on corporate version control systems, or modules stored in air-gapped networks where external access is restricted.
By default, the Go toolchain is configured to use public infrastructure for downloading and verifying modules. Specifically, it uses the public Go module proxy at proxy.golang.org to fetch modules and the checksum database at sum.golang.org to verify their integrity. While this works perfectly for public open-source dependencies, it creates problems when working with private code:
To work with private modules successfully, you need to configure specific environment variables that tell the Go toolchain how to handle private dependencies differently from public ones. The two most important variables are GOPROXY and GOPRIVATE.
GOPROXYThe GOPROXY environment variable controls where the Go toolchain looks for modules when downloading dependencies. It acts as a list of module proxies that Go should consult, in order, when fetching modules.
By default, GOPROXY is set to:
GOPROXY=https://proxy.golang.org,direct
This configuration tells Go to:
proxy.golang.org, the official Google-run public module proxydirect mode, which means Go will fetch the module directly from the source repository using standard version control tools like gitThe public module proxy provides several benefits for public modules:
However, when working with private modules, you have several configuration options:
If you work primarily with private modules or are in an environment without internet access, you can bypass all proxies:
- go env -w GOPROXY=direct
This tells Go to always fetch modules directly from their source repositories using version control tools. While this works, it means you lose the benefits of the public proxy for your public dependencies as well.
Many organizations run their own private module proxies (such as Athens, Artifactory, or Nexus) to cache both public and private modules. You can configure Go to use your organization’s proxy:
- go env -w GOPROXY=https://proxy.company.internal,https://proxy.golang.org,direct
This configuration creates a fallback chain: Go will first check your company’s proxy, then the public proxy, and finally fetch directly if neither has the module.
GOPRIVATE (recommended)The best approach is usually to use GOPRIVATE (explained in the next section) to specify which modules are private, while keeping the default GOPROXY configuration for everything else. This gives you the best of both worlds.
GOPRIVATE and Related VariablesThe GOPRIVATE environment variable is the primary way to tell the Go toolchain which module paths should be treated as private. When a module path matches a pattern in GOPRIVATE, Go will:
To configure GOPRIVATE, you provide a comma-separated list of glob patterns that match your private module paths. For example:
- go env -w GOPRIVATE=github.com/your-company/*
This tells Go that any module under github.com/your-company/ should be treated as private. The * wildcard matches any path segment, so this covers all repositories within your organization.
Multiple patterns: You can specify multiple patterns separated by commas:
- go env -w GOPRIVATE=github.com/your-company/*,gitlab.company.com/*,git.internal.company.com/*
Wildcard patterns: The glob patterns support standard wildcard matching:
github.com/your-company/* - Matches all repositories under your company’s GitHub organization*.internal.company.com/* - Matches all repositories on any internal subdomaingithub.com/your-company/secret-project - Matches only a specific repositoryRelated environment variables: Go provides two additional variables for finer control:
GONOPROXY: Specifies patterns for modules that should never use a proxy (even if GOPROXY is set). By default, it mirrors GOPRIVATE.GONOSUMDB: Specifies patterns for modules that should skip checksum database verification. By default, it also mirrors GOPRIVATE.In most cases, setting GOPRIVATE is sufficient because it automatically sets both GONOPROXY and GONOSUMDB to the same value. However, you can override them independently if needed:
- go env -w GOPRIVATE=github.com/your-company/*
- go env -w GONOPROXY=github.com/your-company/*,github.com/partner-company/*
- go env -w GONOSUMDB=github.com/your-company/*
You should configure private module settings when you encounter any of these scenarios:
Properly configuring GOPROXY, GOPRIVATE, and authentication ensures that your Go module workflow remains secure, reliable, and compatible with both public and private dependencies. The separation between public and private module handling is one of Go’s strengths, allowing teams to seamlessly work with mixed dependency sources while maintaining security and reproducibility.
The go.mod file is the primary module definition file that contains your module’s name, the Go version it requires, and a list of direct dependencies with their versions. It’s human-readable and you can edit it manually if needed, though the go tool typically manages it for you.
The go.sum file, on the other hand, contains cryptographic checksums (SHA-256 hashes) for all downloaded module versions and their dependencies. This file ensures integrity and security by verifying that the exact same module versions are used across different environments and that no one has tampered with the code. You should never edit go.sum manually—it’s automatically maintained by the go tool. Both files should be committed to version control.
You should run go mod tidy whenever you make changes to your code’s dependencies. Specifically, run it:
import statements to your codego get to add new dependencies// indirect comments in go.mod for dependencies that are actually used directlyThe command performs two main tasks: it adds any missing dependencies that are imported in your code but not yet listed in go.mod, and it removes dependencies that are no longer referenced anywhere in your project. This keeps your module files clean and accurate.
There are two main approaches:
Option 1: Using the replace directive (for single modules)
Edit your go.mod file and add a replace directive:
replace github.com/username/modulename => ../local/path/to/modulename
This tells Go to use your local directory instead of downloading from the internet.
Option 2: Using Go workspaces (recommended for multiple modules)
For Go 1.18+, workspaces provide a cleaner solution:
- go work init
- go work use ./your-main-module ./local-dependency-module
This creates a go.work file that tells Go to use local versions without modifying your go.mod files. The workspace configuration is typically added to .gitignore since it’s specific to your local development environment.
An indirect dependency is a module that your project depends on indirectly—meaning it’s a dependency of one of your direct dependencies, not something you import directly in your code. In go.mod, these are marked with // indirect comments:
require (
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5 // indirect
)
In this example, if your code imports cobra but cobra itself depends on pflag, then pflag appears as an indirect dependency. Go tracks indirect dependencies to ensure complete reproducibility of your build. Sometimes a dependency may be marked as indirect temporarily—running go mod tidy will update these markers correctly based on your actual imports.
To upgrade all dependencies to their latest versions, use:
- go get -u ./...
- go mod tidy
The -u flag tells go get to upgrade to the latest minor or patch releases. The ./... pattern means “apply this to all packages in the current module and its subdirectories.”
If you want to upgrade to the latest versions including major version updates (which may include breaking changes), use:
- go get -u=patch ./... # Only patch updates (safest)
- go get -u ./... # Minor and patch updates
- go get module@latest # Specific module to absolute latest
Always run go mod tidy afterward to clean up, and thoroughly test your code after upgrades since new versions may introduce breaking changes or bugs.
By default, Go modules are stored in a global cache (usually $GOPATH/pkg/mod or $GOMODCACHE) that’s shared across all projects on your system. This is efficient because each module version is downloaded only once.
The go mod vendor command creates a vendor/ directory in your project root and copies all dependencies into it:
- go mod vendor
The main differences:
Use vendoring when you need:
Most modern Go projects use the module cache and rely on go.sum for integrity verification rather than vendoring.
Configure the GOPRIVATE environment variable to tell Go which modules are private:
- go env -w GOPRIVATE=github.com/your-company/*
Then set up authentication. The two most common methods are:
SSH authentication (recommended):
- git config --global url."git@github.com:".insteadOf "https://github.com/"
Ensure your SSH keys are added to your Git hosting provider.
HTTPS with tokens:
Create a personal access token from your Git provider and add it to your ~/.netrc file:
machine github.com
login your-username
password your_token_here
Then go get will work normally with your private repositories. The GOPRIVATE setting ensures Go doesn’t send requests to the public proxy or checksum database for these modules, maintaining privacy and avoiding authentication errors.
go work (introduced in Go 1.18) creates workspaces that allow you to work with multiple modules simultaneously. Run go work init to create a go.work file, then add modules with go work use ./module1 ./module2.
Use workspaces when:
go.modUse replace directive when:
The key difference: workspaces are typically local-only (added to .gitignore) and don’t modify your go.mod files, making them ideal for temporary multi-module development. The replace directive modifies go.mod and is usually committed, making it better for permanent redirections or team-wide configuration.
In this tutorial, you created a Go module with a sub-package and used that package within your module. You added remote dependencies, learned how to reference specific module versions using tags, branches, and commits, and explored modern dependency management with go get and go mod tidy. You also learned how the behavior of go get has evolved across Go versions, distinguishing it from go install for clearer intent and reproducible builds.
Additionally, you explored advanced workflows including Go workspaces for managing multiple modules simultaneously, the replace directive for local development and testing, and configuring private module access using GOPROXY and GOPRIVATE environment variables. These tools allow you to organize your code outside of the GOPATH, work seamlessly with both public and private dependencies, and maintain secure, reproducible builds across different development environments.
For more information on Go modules, the Go project has a series of blog posts on Using Go modules and how the Go tools interact with and understand modules. The Go project also has a very detailed and technical reference for Go modules in the Go Modules Reference.
This tutorial is also part of the DigitalOcean How to Code in Go series. The series covers a number of Go topics, from installing Go for the first time to how to use the language itself. For more such Go-related tutorials, check out the following articles:
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
Go (or GoLang) is a modern programming language originally developed by Google that uses high-level syntax similar to scripting languages. It is popular for its minimal syntax and innovative handling of concurrency, as well as for the tools it provides for building native binaries on foreign platforms.
Browse Series: 53 tutorials
Kristin is a life-long geek and enjoys digging into the lowest levels of computing. She also enjoys learning and tinkering with new technologies.
With over 6 years of experience in tech publishing, Mani has edited and published more than 75 books covering a wide range of data science topics. Known for his strong attention to detail and technical knowledge, Mani specializes in creating clear, concise, and easy-to-understand content tailored for developers.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
Full documentation for every DigitalOcean product.
The Wave has everything you need to know about building a business, from raising funding to marketing your product.
Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.
New accounts only. By submitting your email you agree to our Privacy Policy
Scale up as you grow — whether you're running one virtual machine or ten thousand.
From GPU-powered inference and Kubernetes to managed databases and storage, get everything you need to build, scale, and deploy intelligent applications.