Package management in Go
Package management is one of the things Go has always missed. One of the major drawbacks of the previous (pre 1.11) go get was lack of support for managing dependency versions and enabling reproducible builds. The community has developed package managers and tools like Glide, dep and many others serving as de-facto solutions for versioning dependencies.
"I use go get for production builds." — said no one ever.
Go's implementation of package management traces its origins back to Google (which has a giant monolithic repository for all their source code). Let's break down on what's wrong with 'pre - go module' package management tooling.
- Versioning dependencies
- Vendoring dependencies
- The necessity of GOPATH
go get by default didn't support module versioning. The idea behind the first version of go's package management was — no need for module versioning, no need for 3rd-party module repositories, you build everything from your current branch.
Pre Go 1.11, adding a dependency meant cloning that dependency's source code repo in your GOPATH. That was about it. There was no concept of versions. Rather, it always pointed to the current master branch at the time of cloning. Another major issue cropped up when different projects needed different versions of a dependency — which wasn't possible either.
Package vendoring is commonly referred to as the case where dependent packages are stored in the same place as your project. That usually means your dependencies are checked into your source management system, such as Git.
Consider this case — A uses dependency B, which uses a feature of dependency C introduced in version 1.5 of C, B must be able to ensure that A's build uses C 1.5 or later. Pre Go 1.5, there was no mechanism for carrying dependency code alongside commands without rewriting import paths.
Necessity of GOPATH
GOPATH exists for two main reasons:
- In Go, the import declaration references a package via its fully qualified import path. GOPATH exist so that from any directory inside GOPATH/src the go tool can compute the absolute import path of the package in question.
- A location to store dependencies fetched by go get.
What's wrong with this?
- GOPATH doesn’t allow checking out the source of a project in a directory of choice like they are used to with other languages.
- Additionally, GOPATH does not let the developer have more than one copy of a project (or its dependencies) checked out at the same time.
Introducing Go Modules
Go 1.11 introduces preliminary support for Go modules. From Go Wiki,
A module is a collection of related Go packages that are versioned together as a single unit. Modules record precise dependency requirements and create reproducible builds.
Go modules brings three important features built-in,
- go.mod file similar to package.json or Pipfile.
- A machine-generated transitive dependency description - go.sum.
- No more GOPATH limitation. Modules can be in any path.
Relevant discussion thread.
Migrating to Go Modules
To use Go modules, update Go to version >= 1.11. Since GOPATH is going away, one can activate module support in one of these two ways:
- Invoke the go command in a directory outside of the GOPATH/src tree, with a valid go.mod file in the current directory.
- Go modules don't work if source is under GOPATH. To override this behaviour, invoke the go command with GO111MODULE=on environment variable set.
Let's start porting by following these simple steps:
- As GOPATH isn't necessary anymore, move the module out of GOPATH.
- From the project root, create the initial module definition - go mod init github.com/username/repository. The best part is, go mod automatically converts dependencies from existing package managers like dep, Gopkg, glide and six others. This will create a file called go.mod with the module name and dependencies with its versions.
- Run go build to create a go.sum file which contains the expected cryptographic checksums of the content of specific module versions. This is to ensure that future downloads of these modules retrieve the same bits as the first download. Note that go.sum is not a lock file.
Note on versioning: To maintain backward compatibility, if the module is version v2 or higher, the major version of the module must be included as a /vN at the end of the module paths used in go.mod files (e.g., module github.com/username/repository/v2
go list -m all lists the current module and all its dependencies.
In the go list output, the current module, also known as the main module, is always the first line, followed by dependencies sorted by module path.
List available versions of a package
go list -m -versions github.com/username/repository lists available versions of a package.
Add a dependency
Adding a dependency is implicit. After importing a dependency in code, running go build or go test command gets the latest version of the module and adds it to go.mod file. If you would like to add a dependency explicitly, rungo get github.com/username/repository.
Upgrade/downgrade a dependency
go get firstname.lastname@example.org downloads and sets the specific version of the dependency and updates go.mod file.
When using modules, the go command completely ignores vendor directories. For backward compatibility with older versions of Go, or to ensure that all files used for a build are stored together in a single file tree, rungo mod vendor.
This creates a directory named vendor in the root directory of the main module and stores all the packages from dependency modules there.
Note: To build using the main module's top-level vendor directory, run 'go build -mod=vendor'.
Remove unused dependencies
go mod tidy trims unused dependencies and updates go.mod file.
Is GOPATH not needed anymore?
No. Farewell GOPATH.
Which version is pulled by default?
The go.mod file and the go command more generally use semantic versions as the standard form for describing module versions, so that versions can be compared to determine which should be considered earlier or later than another. A module version like v1.2.3 is introduced by tagging a revision in the underlying source repository. Untagged revisions can be referred to using a "pseudo-version" like v0.0.0-yyyymmddhhmmss-abcdefabcdef, where the time is the commit time in UTC and the final suffix is the prefix of the commit hash.
Should go.sum be checked into version control?