Report this

What is the reason for this report?

How to Use Go Modules

Updated on April 1, 2026
How to Use Go Modules

Introduction

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:

  • Go modules allow you to place projects anywhere on your filesystem, not just in GOPATH. The go.mod file defines your module and its dependencies, while go.sum contains cryptographic checksums that ensure security and reproducibility.
  • Always commit both go.mod and go.sum to version control to ensure consistent builds across different environments and prevent dependency tampering.
  • Run 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.
  • In modern Go, use 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.
  • Always specify versions explicitly using @latest, @v1.2.3, @branch-name, or @commit-hash when adding dependencies. This ensures reproducible builds and makes your dependency requirements clear.
  • Go workspaces let you develop multiple related modules simultaneously without modifying 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.
  • Configure the 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 dependencies marked with // 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.
  • Use the 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.
  • Go modules support Git features seamlessly, allowing you to reference specific tags, branches, and commits using @ syntax. This gives you fine-grained control over dependency versions, from stable releases to specific commits.

Prerequisites

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.

Creating a New Module

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:

  1. mkdir projects
  2. 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:

  1. 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:

  1. go mod init mymodule

Note: In production scenarios, you would typically run:

  1. 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:

Output
go: 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.

Understanding the go.mod File

When 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:

  1. nano go.mod

The contents will look similar to this, which isn’t much:

projects/mymodule/go.mod
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.

Adding Go Code to Your Module

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:

  1. 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:

projects/mymodule/main.go
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:

  1. go run main.go

Running the command will print the Hello, Modules! text as defined in the code:

Output
Hello, Modules!

Tip: In module-aware mode (default in modern Go versions), you can also run the entire module using:

  1. 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.

Adding a Package to Your Module

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:

  1. 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:

  1. cd mypackage
  2. 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:

projects/mymodule/mypackage/mypackage.go
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:

projects/mymodule/main.go

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:

  1. 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:

Output
Hello, 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.

Adding a Remote Module as a Dependency

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:

  1. 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:

  1. 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:

projects/mymodule/go.mod
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:

projects/mymodule/main.go
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:

  1. 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:

Output
Calling 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.

Using a Specific Version of a Module

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:

  1. 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:

  1. 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:

  1. 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:

projects/mymodule/go.mod
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:

  1. 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:

  1. 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.

Working with Multiple Modules Using Go Workspaces

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.

Creating a Workspace

To create a workspace, navigate to a directory that will contain your related modules and run:

  1. go work init

This will create a go.work file in the current directory.

Adding Modules to the Workspace

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:

  1. 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
)

How Workspaces Work

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:

  • Develop multiple modules together
  • Test changes across modules without publishing new versions
  • Avoid temporary replace directives during development

When to Use Workspaces

Workspaces are particularly useful in the following scenarios:

  • Monorepos: Managing multiple services or libraries in a single repository
  • Library + Application Development: Developing a library and its consuming application at the same time
  • Local Development: Testing changes across modules before releasing a new version

Workspaces vs replace

Before 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.

Using the replace Directive

The 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.

Basic Syntax

The replace directive maps one module path to another location:

replace <module-path> => <replacement-path>

The replacement can be:

  • A local directory
  • A different module path
  • A specific version of another module

Example 1: Using a Local Module

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:

  • Developing a library and application together
  • Testing changes before pushing or tagging a release

Example 2: Using a Forked Dependency

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.

Example 3: Pinning to a Local Version in a Monorepo

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.

Important Considerations

  • The replace directive only affects your local module. It is not applied transitively to other modules that depend on your code.
  • Because of this, replace directives are often used for development purposes only and may be removed before publishing a module.
  • If you commit a 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.

Understanding go get Behavior Across Go Versions

The 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.

The Legacy Behavior (Go 1.15 and Earlier)

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:

  1. go get github.com/spf13/cobra

Would perform several actions at once:

  • Download the cobra package and its dependencies
  • Add or update the dependency in your go.mod file (if inside a module)
  • Build and install the 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.

The Transition Period (Go 1.16–1.17)

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 directory

This separation clarified the intent of each operation. If you wanted to install a CLI tool like cobra, you would now use:

  1. 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:

  1. go get github.com/spf13/cobra@latest

This change immediately improved several aspects of Go development:

  • Clearer intent: The command you use explicitly states whether you’re managing dependencies or installing tools
  • Reproducible builds: Specifying versions with @version syntax became standard practice, ensuring consistent dependency resolution
  • Cleaner module files: Only actual code dependencies appear in go.mod, not tools you happen to have installed

During this transition period, the Go toolchain displayed deprecation warnings to help developers adjust their workflows.

Modern Behavior (Go 1.21 and Later)

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:

  1. go get github.com/spf13/cobra@latest
  2. 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:

  • Removes any dependencies that are no longer referenced in your code
  • Adds any missing dependencies that are imported but not yet listed
  • Updates the go.sum file with cryptographic checksums for security and reproducibility
  • Resolves and records indirect dependencies (dependencies of your dependencies)

When 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.

Working with Private Modules

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:

  • Privacy concerns: Private module paths are sent to public proxies, potentially exposing sensitive project information
  • Authentication failures: Public proxies cannot access private repositories that require credentials
  • Checksum verification errors: The public checksum database doesn’t have records for private modules, causing verification to fail

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.

Understanding GOPROXY

The 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:

  1. First, try to download modules from proxy.golang.org, the official Google-run public module proxy
  2. If that fails (returns a 404 or 410 error), fall back to direct mode, which means Go will fetch the module directly from the source repository using standard version control tools like git

The public module proxy provides several benefits for public modules:

  • Fast downloads: Modules are cached and served from Google’s CDN infrastructure
  • Availability: Modules remain available even if the original repository is deleted or moved
  • Immutability: Once a version is cached, it never changes, ensuring reproducibility

However, when working with private modules, you have several configuration options:

Option 1: Disable the proxy entirely for all modules

If you work primarily with private modules or are in an environment without internet access, you can bypass all proxies:

  1. 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.

Option 2: Use a private module proxy

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:

  1. 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.

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.

The 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:

  • Skip the module proxy entirely and fetch directly from the source
  • Not check the checksum database for verification
  • Not send any information about the module to public services

To configure GOPRIVATE, you provide a comma-separated list of glob patterns that match your private module paths. For example:

  1. 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:

    1. 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 subdomain
    • github.com/your-company/secret-project - Matches only a specific repository
  • Related 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:

  1. go env -w GOPRIVATE=github.com/your-company/*
  2. go env -w GONOPROXY=github.com/your-company/*,github.com/partner-company/*
  3. go env -w GONOSUMDB=github.com/your-company/*

When to Use Private Module Configurations

You should configure private module settings when you encounter any of these scenarios:

  • Corporate development: Working with proprietary libraries and internal tools that are not open source
  • Client projects: Using repositories from clients or partners with restricted access
  • Pre-release code: Developing with unreleased or beta versions of modules that aren’t public yet
  • Compliance requirements: Operating in regulated industries where code must remain on internal infrastructure
  • Air-gapped environments: Working in networks isolated from the public internet for security reasons
  • Custom proxies: Using organizational artifact repositories or module proxies (Athens, Artifactory, etc.)

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.

FAQs

1. What is the difference between go.mod and go.sum?

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.

2. When should I run go mod tidy?

You should run go mod tidy whenever you make changes to your code’s dependencies. Specifically, run it:

  • After adding new import statements to your code
  • After removing imports or deleting code that used dependencies
  • After running go get to add new dependencies
  • Before committing your changes to version control
  • When you see // indirect comments in go.mod for dependencies that are actually used directly

The 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.

3. How do I use a local version of a module instead of the one on the internet?

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:

  1. go work init
  2. 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.

4. What is an indirect dependency in go.mod?

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.

5. How do I upgrade all dependencies to their latest versions?

To upgrade all dependencies to their latest versions, use:

  1. go get -u ./...
  2. 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:

  1. go get -u=patch ./... # Only patch updates (safest)
  2. go get -u ./... # Minor and patch updates
  3. 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.

6. What is the difference between go mod vendor and using the module cache?

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:

  1. go mod vendor

The main differences:

  • Module cache: Global, shared across projects, automatically managed, requires internet access for first download
  • Vendor directory: Local to your project, self-contained, can work offline, increases repository size

Use vendoring when you need:

  • Complete control over dependencies (useful in corporate environments)
  • Guaranteed offline builds
  • Compliance requirements to check in all code
  • Protection against disappearing or modified upstream modules

Most modern Go projects use the module cache and rely on go.sum for integrity verification rather than vendoring.

7. How do I use Go modules with a private Git repository?

Configure the GOPRIVATE environment variable to tell Go which modules are private:

  1. go env -w GOPRIVATE=github.com/your-company/*

Then set up authentication. The two most common methods are:

SSH authentication (recommended):

  1. 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.

8. What is go work and when should I use it instead of the replace directive?

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:

  • You’re actively developing multiple related modules together
  • You want to test changes across modules before releasing
  • You’re working in a monorepo with multiple Go modules
  • You need a temporary setup for local development that won’t affect go.mod

Use replace directive when:

  • You need to permanently redirect a module to a fork
  • The replacement should be committed and shared with your team
  • You’re using Go versions older than 1.18
  • You want the replacement to persist in the project configuration

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.

Conclusion

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.

Learn more about our products

Tutorial Series: How To Code in Go

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.

About the author(s)

Kristin Davidson
Kristin Davidson
Author
Bit Transducer
See author profile

Kristin is a life-long geek and enjoys digging into the lowest levels of computing. She also enjoys learning and tinkering with new technologies.

Rachel Lee
Rachel Lee
Editor
Technical Editor
See author profile
Manikandan Kurup
Manikandan Kurup
Editor
Senior Technical Content Engineer I
See author profile

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.

Category:

Still looking for an answer?

Was this helpful?


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!

Creative CommonsThis work is licensed under a Creative Commons Attribution-NonCommercial- ShareAlike 4.0 International License.
Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Start building today

From GPU-powered inference and Kubernetes to managed databases and storage, get everything you need to build, scale, and deploy intelligent applications.