Dependency management in any language or ecosystem is more nuanced that it could initially look. Given that Go's approach to dependency management has some fundamental differences with other languages, I think it's worth explaining the fundamentals and giving some guidelines for the basic operations. During the last few years the dependency management story in Go has changed a lot, this post covers the Go versions 1.17 or higher.
The post is divided in 3 parts:
Basic concepts
In Go the basic unit of work for publishing and versioning code is called
module. Notice that a package is different from a
module. A package is: a collection of source files in the same directory that are compiled together
, while a module is: a collection of packages that are released, versioned, and distributed together
.
Go modules are identified by their name and a version, for
instance: example.com/moda@v0.0.0
.
The version of a module must follow semantinc versioning.
Different major versions of the same
module are considered different modules, so you can use multiple major versions
of the same module at the same time.
A module can use other modules, those modules can, in turn, use other modules and so on.If a module A uses another module B, we say the module A depends on the module B.
For a given module, the modules it depends on, that is the modules it directly uses, are called direct dependencies, in turn, the modules that those direct dependencies depend on are called transitive dependencies or indirect dependencies.
The dependencies used by a given module are listed in its go.mod
file.
The go.mod
file is modified by running commands provided by the
go tool, for example: go get example.com/moda@v0.0.0
.
The go.sum file contains the crytographic hashes of the direct and indirect dependencies of a module. This information is used by the Go tool to detect if any of those dependencies has been tampered with.
Guideline
Fundamentals
In order to understand how Go manages dependencies is important to keep in mind the following main ideas:
-
As stated by Russ Cox, the Go modules system aims to produce high-fidelity builds:
in which the dependencies a user builds are as close as possible to the ones the author developed against
. -
When you add a dependency to your module, you specify the minimum version in a fixed
major
version that your module can use. For instance, if you add a dependency with the versionv1.0.1
, what you are saying to the Go tool is: my module prefers to use the versionv1.0.1
and that's the version my module is tested with, but any version with a greater or equalpatch
to 1 or greater or equalminor
to 0 is valid.
Given the points 1 and 2, and unless you explicitly force a concrete version, the Go commands will always select the minimum valid version of any indirect dependency.
About this guideline
Drawing the dependencies of a module as a graph is useful for visualizing how the commands of the Go tool affect those dependencies. This guide uses graphs similar the following one to illustrate these effects:
The vertices of the graph represent the versions of the possible direct and
indirect dependencies of the module modroot
.
The red arrows represent the direct dependencies used by the modroot
,
The blue arrows represent the indirect (aka transitive) dependencies.
The black arrows of the graph represent which version of the modc
is
being used by a concrete version of the modules moda
and modb
.
All the commands in the examples affect the dependencies of the
module modroot
.
The examples assume that if we add a dependency, the dependency is actually
used somewhere in the code and, if we remove a dependency, there is no code
that uses that dependency anymore. This is important because the command go mod tidy
, executed at the end of each example to keep the go.mod and go.sum
files consistent, takes into account if a dependency is actually used in the
code when updating those files.
Basic Commands
-
Initialize a new module
-
Command:
go mod init module-name
-
Example:
go mod init example.com/modroot
-
Notes:
This command will just create a go.mod file that will be edited by the go tool when adding, removing or updating dependencies.
-
-
Adding a direct dependency
-
Command:
go get package@version
-
Options:
- if no
@version
is specified the tool will select the newest version.
- if no
-
Example
Before:
Commands:
go get example.com/moda@v1.0.0 go get example.com/modb@v1.0.0 go mod tidy
After:
-
-
Removing a direct dependency
-
Command:
go get package@none
-
Example
Before:
Commands:
go get example.com/moda@none go mod tidy
After:
-
-
Updating a direct dependency
-
Command:
go get package@version
-
Options:
- If no version is provided the command will update the dependency to the last version in the same major version.
-
Example
Before:
Commands:
go get example.com/moda@v1.0.1 go mod tidy
After:
-
-
Force update direct and indirect dependencies
-
Command:
go get -u package@version
-
Options: If no version is provided the command will update the direct dependency to the last version in the same major version. But notice that the command will always update the indirect dependencies.
-
Example
Before:
Commands:
go get -u example.com/modb go mod tidy
After:
-
Notes:
Notice that this command breaks the idea of "high-fidelity" builds, as your program could end up using indirect dependencies that no direct dependency is actually using, therefore, my advice is to not use it unless you absolutely need to.
-