【Golang】依赖管理 - go module

Posted by 西维蜀黍 on 2020-01-12, Last Modified on 2023-02-28

Background

GOPATH的痛

当有多个项目时,不同项目对于依赖库的版本需求不一致时,无法在一个GOPATH下面放置不同版本的依赖项。典型的例子:当有多项目时候,A项目依赖C 1.0.0,B项目依赖C 2.0.0,由于没有依赖项版本的概念,C 1.0.0和C 2.0.0无法同时在GOPATH下共存,解决办法是分别为A项目和B项目设置GOPATH,将不同版本的C源代码放在两个GOPATH中,彼此独立(编译时切换),或者C 1.0.0和C 2.0.0两个版本更改包名。无论哪种解决方法,都需要人工判断更正,不具备便利性。

As of Go 1.11, the go command enables the use of modules when the current directory or any parent directory has a go.mod, provided the directory is outside $GOPATH/src. (Inside $GOPATH/src, for compatibility, the go command still runs in the old GOPATH mode, even if a go.mod is found.

Starting in Go 1.13, module mode will be the default for all development.

go mod 使用

$ go mod
Go mod provides access to operations on modules.

Note that support for modules is built into all the go commands,
not just 'go mod'. For example, day-to-day adding, removing, upgrading,
and downgrading of dependencies should be done using 'go get'.
See 'go help modules' for an overview of module functionality.

Usage:

	go mod <command> [arguments]

The commands are:

	download    download modules to local cache
	edit        edit go.mod from tools or scripts
	graph       print module requirement graph
	init        initialize new module in current directory
	tidy        add missing and remove unused modules
	vendor      make vendored copy of dependencies
	verify      verify dependencies have expected content
	why         explain why packages or modules are needed

Use "go help mod <command>" for more information about a command.
# add missing and remove unused modules
$ go mod tidy

# Prune any no-longer-needed dependencies from go.mod and add any dependencies needed for other combinations of OS, architecture, and build tags
$ go mod init

# download modules to local cache
$ go mod download

如何使用 go mod

在默认情况下,$GOPATH 默认情况下是不支持 go mudules 的。

在.zshrc 或者.bashrc中声明环境变量:

# go module
export GO111MODULE=on

使声明立刻生效(以.zshrc 为例):

$ source ~/.zshrc

我们 $GOPATH 以外的目录创建一个 testmod 的包:

$ mkdir testmod
$ cd testmod

$ echo 'package testmod
		import "fmt"
	func SayHello(name string) string {
   return fmt.Sprintf("Hello, %s", name)
}' >> testmod.go初始化 module:

git mod init - 初始化一个go module 项目

$ go mod init github.com/sw/testmod
go: creating new go.mod: module github.com/sw/testmod

以上命令会在项目中创建一个 go.mod 文件,初始化内容如下:

module github.com/sw/testmod

这时,我们的项目已经成为了一个 module 了。

添加依赖

在该目录下执行 go build netproc.go ,就可以了。你将看到:

$ go build netproc.go
go: finding github.com/mitchellh/go-wordwrap latest
go: finding github.com/maruel/panicparse/stack latest
go: finding github.com/nsf/termbox-go latest

go命令(go build, go testgo run甚至go list)被执行时,就会下载依赖包,并把依赖信息添加到 go.mod 文件中,自动修改后 go.mod 文件新内容如下:

module github.com/silenceshell/netproc

require (
    github.com/gizak/termui v2.2.0+incompatible
    github.com/maruel/panicparse v1.1.1 // indirect
    github.com/mattn/go-runewidth v0.0.3 // indirect
    github.com/mitchellh/go-wordwrap v0.0.0-20180828145344-9e67c67572bc // indirect
    github.com/nsf/termbox-go v0.0.0-20180819125858-b66b20ab708e // indirect
    github.com/spf13/pflag v1.0.2 // indirect
)

可见,go.mod中记录了依赖包及其版本号。


或者,新建一个 server.go 文件,写入以下代码:

package main

import (
	"net/http"
	
	"github.com/labstack/echo"
)

func main() {
	e := echo.New()
	e.GET("/", func(c echo.Context) error {
		return c.String(http.StatusOK, "Hello, World!")
	})
	e.Logger.Fatal(e.Start(":1323"))
}

执行 go run server.go 运行代码会发现 go mod 会自动查找依赖自动下载:

$ go run server.go
go: finding github.com/labstack/echo v3.3.10+incompatible
go: downloading github.com/labstack/echo v3.3.10+incompatible
go: extracting github.com/labstack/echo v3.3.10+incompatible
go: finding github.com/labstack/gommon/color latest
go: finding github.com/labstack/gommon/log latest
go: finding github.com/labstack/gommon v0.2.8
# 此处省略很多行
...

   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v3.3.10-dev
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\
⇨ http server started on [::]:1323

现在查看go.mod 内容:

$ cat go.mod

module hello

go 1.12

require (
	github.com/labstack/echo v3.3.10+incompatible // indirect
	github.com/labstack/gommon v0.2.8 // indirect
	github.com/mattn/go-colorable v0.1.1 // indirect
	github.com/mattn/go-isatty v0.0.7 // indirect
	github.com/valyala/fasttemplate v1.0.0 // indirect
	golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a // indirect
)

When it encounters an import of a package not provided by any module in go.mod, the go command automatically looks up the module containing that package and adds it to go.mod, using the latest version. (“Latest” is defined as the latest tagged stable (non-prerelease) version, or else the latest tagged prerelease version, or else the latest untagged version.)

go 会自动生成一个 go.sum 文件来记录 dependency tree:

$ cat go.sum
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
...
  • When needed, more specific versions of dependencies can be chosen with commands such as go get foo@v1.2.3, go get foo@master (foo@default with mercurial), go get foo@e3702bed2, or by editing go.mod directly.

Getting a specific dependency version

You can get a specific version of a dependency module by specifying its version in the go get command. The command updates the require directive in your go.mod file (though you can also update that manually).

You might want to do this if:

  • You want to get a specific pre-release version of a module to try out.
  • You’ve discovered that the version you’re currently requiring isn’t working for you, so you want to get a version you know you can rely on.
  • You want to upgrade or downgrade a module you’re already requiring.

Here are examples for using the go get command:

  • To get a specific numbered version, append the module path with an @ sign followed by the version you want:

    $ go get example.com/theirmodule@v1.3.4
    
  • To get the latest version, append the module path with @latest:

    $ go get example.com/theirmodule@latest
    

The following go.mod file require directive example (see the go.mod reference for more) illustrates how to require a specific version number:

require example.com/theirmodule v1.3.4

Version Selection

If you add a new import to your source code that is not yet covered by a require in go.mod, most go commands like ‘go build’ and ‘go test’ will automatically look up the proper module and add the highest version of that new direct dependency to your module’s go.mod as a require directive. For example, if your new import corresponds to dependency M whose latest tagged release version is v1.2.3, your module’s go.mod will end up with require M v1.2.3, which indicates module M is a dependency with allowed version >= v1.2.3 (and < v2, given v2 is considered incompatible with v1).

The minimal version selection algorithm is used to select the versions of all modules used in a build. For each module in a build, the version selected by minimal version selection is always the semantically highest of the versions explicitly listed by a require directive in the main module or one of its dependencies.

As an example, if your module depends on module A which has a require D v1.0.0, and your module also depends on module B which has a require D v1.1.1, then minimal version selection would choose v1.1.1 of D to include in the build (given it is the highest listed require version). This selection of v1.1.1 remains consistent even if sometime later a v1.2.0 of D becomes available. This is an example of how the modules system provides 100% reproducible builds. When ready, the module author or user might choose to upgrade to the latest available version of D or choose an explicit version for D.

go.mod 语法

A module is defined by a tree of Go source files with a go.mod file in the tree’s root directory. Module source code may be located outside of GOPATH. There are four directives: module, require, replace, exclude.

Here is an example go.mod file defining the module github.com/my/thing:

module github.com/my/thing

require (
    github.com/some/dependency v1.2.3
    github.com/another/dependency/v4 v4.0.0
)

A module declares its identity in its go.mod via the module directive, which provides the module path. The import paths for all packages in a module share the module path as a common prefix. The module path and the relative path from the go.mod to a package’s directory together determine a package’s import path.

For example, if you are creating a module for a repository github.com/user/mymod that will contain two packages with import paths github.com/user/mymod/foo and github.com/user/mymod/bar, then the first line in your go.mod file typically would declare your module path as module github.com/user/mymod, and the corresponding on-disk structure could be:

mymod
|-- bar
|   `-- bar.go
|-- foo
|   `-- foo.go
`-- go.mod

In Go source code, packages are imported using the full path including the module path. For example, if in our example above, we declared the module identity in go.mod as module github.com/user/mymod, a consumer could do:

import "github.com/user/mymod/bar"

This imports package bar from the module github.com/user/mymod.

exclude and replace directives only operate on the current (“main”) module. exclude and replace directives in modules other than the main module are ignored when building the main module. The replace and exclude statements, therefore, allow the main module complete control over its own build, without also being subject to complete control by dependencies. (See FAQ below for a discussion of when to use a replace directive).

Discovering available updates

You can check to see if there are newer versions of dependencies you’re already using in your current module. Use the go list command to display a list of your module’s dependencies, along with the latest version available for that module. Once you’ve discovered available upgrades, you can try them out with your code to decide whether or not to upgrade to new versions.

For more about the go list command, see go list -m.

Here are a couple of examples.

  • List all of the modules that are dependencies of your current module, along with the latest version available for each:

    $ go list -m -u all
    
  • Display the latest version available for a specific module:

    $ go list -m -u example.com/theirmodule
    

Upgrade and Downgrade Dependencies

Day-to-day upgrading and downgrading of dependencies should be done using ‘go get’, which will automatically update the go.mod file. Alternatively, you can edit go.mod directly.

In addition, go commands like ‘go build’, ‘go test’, or even ‘go list’ will automatically add new dependencies as needed to satisfy imports (updating go.mod and downloading the new dependencies).

To upgrade a dependency to the latest version:

go get example.com/package

To upgrade a dependency and all its dependencies to the latest version:

go get -u example.com/package

To view available minor and patch upgrades for all direct and indirect dependencies:

go list -u -m all

To view available minor and patch upgrades only for the direct dependencies, run:

go list -u -f '{{if (and (not (or .Main .Indirect)) .Update)}}{{.Path}}: {{.Version}} -> {{.Update.Version}}{{end}}' -m all 2> /dev/null

To upgrade to the latest version for all direct and indirect dependencies of the current module, the following can be run from the module root directory:

  • go get -u ./... to use the latest minor or patch releases (and add -t to also upgrade test dependencies)
  • go get -u=patch ./... to use the latest patch releases (and add -t to also upgrade test dependencies)

go get foo updates to the latest version of foo. go get foo is equivalent to go get foo@latest — in other words, @latest is the default if no @ version is specified.

说明

Version Selection

If you add a new import to your source code that is not yet covered by a require in go.mod, most go commands like ‘go build’ and ‘go test’ will automatically look up the proper module and add the highest version of that new direct dependency to your module’s go.mod as a require directive.

For example, if your new import corresponds to dependency M whose latest tagged release version is v1.2.3, your module’s go.mod will end up with require M v1.2.3, which indicates module M is a dependency with allowed version >= v1.2.3 (and < v2, given v2 is considered incompatible with v1).

The minimal version selection algorithm is used to select the versions of all modules used in a build. For each module in a build, the version selected by minimal version selection is always the semantically highest of the versions explicitly listed by a require directive in the main module or one of its dependencies.

As an example, if your module depends on module A which has a require D v1.0.0, and your module also depends on module B which has a require D v1.1.1, then minimal version selection would choose v1.1.1 of D to include in the build (given it is the highest listed require version). This selection of v1.1.1 remains consistent even if sometime later a v1.2.0 of D becomes available. This is an example of how the modules system provides 100% reproducible builds. When ready, the module author or user might choose to upgrade to the latest available version of D or choose an explicit version for D.

Go Module 的科学性

分析 1

在go mudules 出现之前,通常,一个项目中会有很多个包,而项目内有些包需要依赖项目内其它包。

假设项目有个包,它的包名为 sw/mypackage。当项目内的其它包需要引用这个包时,就可以通过以下引用:

import myproject/mypackage

但你有没有想过,当别的项目需要引用你的项目中的某些包,那么就需要远程下载依赖包了,这时就需要项目的仓库地址引用,比如下面这样:

import github.com/sw/myproject/mypackage

go modules 发布之后,就完全统一了包引用的地址

如上面我们说的创建 go.mod 文件后,初始化内容的第一行就是我们说的项目依赖路径,通常来说该地址就是项目的仓库地址,所有需要引用项目包的地址都填写这个地址,无论是内部之间引用还是外部引用。

分析 2

在npm 中,我们通常会遇到这样的情况:

$ tree
.
└── node_modules
    ├── babel-traverse
       └── ...
       └── node_modules
           └── ms
               └── ...
    └── basic-auth
        ├── ...
        └── node_modules
            └── ms
                └── ...

我们项目需要依赖于 babel-traversebasic-auth包,而这两个包又都需要依赖一个名为 ms 的第三方包,这时,这个 ms包就会在文件系统中存在两份。

可以想象,如果某个第三方包,会被我们项目中依赖的包依赖 N 次,则这个第三方包就会在文件系统中存在 N 份。

go module 的科学性就在于此:

  • 所有的依赖包都存储在$GOPATH中,不管是我们项目本身依赖的第三方包,还是我们项目本身依赖的第三方包所依赖的第三方包。
  • 而且,当同一个第三方包存在多个版本会被依赖时(甚至是同一个版本的不同 commit),Go 会以{包名称}@{版本号}-{日期}-{hash} 的格式命名该包所在是文件夹,比如 sys@v0.0.0-20190422165155-953cdadca894sys@v0.0.0-20190813064441-fde4db37ae7a

Go mod 的子命令

golang 提供了 go mod命令来管理包。

go mod 有以下子命令:

命令 说明
download download modules to local cache(下载依赖包)
edit edit go.mod from tools or scripts(编辑go.mod
graph print module requirement graph (打印模块依赖图)
init initialize new module in current directory(在当前目录初始化mod)
tidy add missing and remove unused modules(拉取缺少的模块,移除不用的模块)
vendor make vendored copy of dependencies(将依赖复制到vendor下)
verify verify dependencies have expected content (验证依赖是否正确)
why explain why packages or modules are needed(解释为什么需要依赖)

比如,运行:

$ go mod tidy

下载非官方依赖

Problem

$ go get git.xxx.com/yyy/core-logic
go: downloading git.xxx.com/yyy/core-logic v0.0.41
verifying git.xxx.com/yyy/core-logic@v0.0.41: git.xxx.com/yyy/core-logic@v0.0.41: reading https://sum.golang.org/lookup/git.xxx.com/yyy/core-logic@v0.0.41: 410 Gone

# or
$ go get git.xxx.com/core-logic
go: git.xxx.com/core-logic@v0.0.41: reading git.xxx.com/core-logic/go.mod at revision v0.0.41: unknown revision v0.0.41

Solution

$ go env -w GOPRIVATE=git.xxx.com

# Try again, it would be working
$ go get git.xxx.com/yyy/core-logic

Reference