go mod 命令

1
2
3
4
5
6
7
8
download    download modules to local cache (下载依赖的module到本地cache))
edit        edit go.mod from tools or scripts (编辑go.mod文件)
graph       print module requirement graph (打印模块依赖图))
init        initialize new module in current directory (再当前文件夹下初始化一个新的module, 创建go.mod文件))
tidy        add missing and remove unused modules (增加丢失的module,去掉未用的module)
vendor      make vendored copy of dependencies (将依赖复制到vendor下)
verify      verify dependencies have expected content (校验依赖)
why         explain why packages or modules are needed (解释为什么需要依赖)

go 命令行工具会根据 go.mod 里面指定好的依赖的模块版本来下载相应的依赖模块。在你的代码中 import 了一个包,但 go.mod 文件里面又没有指定这个包的时候,go 命令行工具会自动寻找包含这个代码包的模块的最新版本,并添加到 go.mod 中(这里的 " 最新 " 指的是:它是最近一次被 tag 的稳定版本(即非预发布版本,non-prerelease),如果没有,则是最近一次被 tag 的预发布版本,如果没有,则是最新的没有被 tag 过的版本)。

所有的升级操作都需要人工确认并执行,go 官方的工具不会自动升级

注意,go modules下载的包在 GOPATH/pkg/mod.

语义版本 Semantic versioning

首先所有的模块都必须遵循语义化版本规则:

  • major: 做了不兼容的升级,升这个
  • minor: 向前兼容的升级,升这个
  • patch: 未改变功能,只是修了 bug 之类的,升这个
  • 开头的那个 v 是不能省略掉的
  • 在 import 的包主版本号 v0 或 v1 时,import 路径不能写版本号
  • 当主版本号大于等于 v2 时,这个 Module 的 import path 必须在尾部加上 /vN。
    • 在 go.mod 文件中: module github.com/my/mod/v2
    • 在 require 的时候: require github.com/my/mod/v2 v2.0.0
    • 在 import 的时候: import "github.com/my/mod/v2/mypkg"

根据语义化版本的要求,v0 是不需要保证兼容性的,可以随意的引入破坏性变更,所以不需要显式的写出来;而省略 v1 更大程度上是现实的考虑,毕竟 99% 的包都不会有 v2,同时考虑到现有代码库的兼容,省略 v1 是一个合情合理的决策。

如果包的作者还没有标记版本,默认为 v0.0.0

在介绍版本控制之前,我们要先明确一点,如果上层目录和下层目录的go.mod里有相同的package规则,那么上层目录的无条件覆盖下层目录,目的是为了main module的构建不会被依赖的package所影响。

那么我们看看go.mod长什么样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
module github.com/chromedp/chromedp

require (
	github.com/chromedp/cdproto v0.0.0-20180713053126-e314dc107013
	github.com/disintegration/imaging v1.4.2
	github.com/gorilla/websocket v1.2.0
	github.com/knq/sysutil v0.0.0-20180306023629-0218e141a794
	github.com/mailru/easyjson v0.0.0-20180606163543-3fdea8d05856
	golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81
)

前面部分是包的名字,也就是import时需要写的部分,而空格之后的是版本号,版本号遵循如下规律:

1
2
3
4
vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef
vX.0.0-yyyymmddhhmmss-abcdefabcdef
vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdefabcdef
vX.Y.Z

golang.org/x/text 版本 v0.0.0-20170915032832-14c0d48ead0c 是伪版本的示例, 它是特定无标记提交的 go 命令的版本语法,v0.0.0是为了符合go module版本控制的规范做的workround; 20180606163543-3fdea8d05856是代表了这个依赖包当初被引入时最新一次commit的时间和hash值(取了前12位),应该是通过Rev这个commit反查出来的

这带来了2个痛点:

  • 目标库需要打上符合要求的tag,如果tag不符合要求不排除日后出现兼容问题
  • 如果目标库没有打上tag,那么就必须毫无差错的编写大串的版本信息,大大加重了使用者的负担

基于以上原因,现在可以直接使用commit的hash来指定版本,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 使用go get时
go get github.com/mqu/go-notify@ef6f6f49

# 在go.mod中指定
module my-module

require (
  // other packages
  github.com/mqu/go-notify ef6f6f49
)

随后我们运行go build或go mod tidy,这两条命令会整理并更新go.mod文件,更新后的文件会是这样:

1
2
3
4
5
6
module my-module

require (
    github.com/mattn/go-gtk v0.0.0-20181205025739-e9a6766929f6 // indirect
    github.com/mqu/go-notify v0.0.0-20130719194048-ef6f6f49d093
)

可以看到hash信息自动扩充成了符合要求的版本信息,今后可以依赖这一特性简化包版本的指定。

对于hash信息只有两个要求:

  1. 指定hash信息时不要在前面加上v,只需要给出commit hash即可
  2. hash至少需要8位,与git等工具不同,少于8位会导致go mod无法找到包的对应版本,推荐与go mod保持一致给出12位长度的hash

也就是版本号+时间戳+hash,我们自己指定版本时只需要制定版本号即可,没有版本tag的则需要找到对应commit的时间和hash值。

默认使用最新版本的package。

现在我们要修改依赖关系了,我们想使用chromedp 的v0.1.0版本,怎么办呢?

只需要如下命令:

1
go mod edit -require="github.com/chromedp/chromedp@v0.1.0"

@后面加上你需要的版本号。go.mod已经修改了:

1
2
3
module test

require github.com/chromedp/chromedp v0.1.0

关于上述规则的后两点,主要是为了前后版本兼容,下面具体讲一下.

如果你使用和发布的包没有版本tag或者处于1.x版本,那么你可能体会不到什么问题,因为go mod所支持的格式从始至终是遵循semver的,主要的区别体现在v2.0.0以及更高版本的包上。

相同名字的对象应该向后兼容,然而按照语义化版本的约定,当出现v2.0.0的时候一定表示发生了重大变化,很可能无法保证向后兼容,这时候应该如何处理呢?

答案很简单,我们为包的导入路径的末尾附加版本信息即可,例如:

1
2
3
4
5
6
7
module my-module/v2

require (
  some/pkg/v2 v2.0.0
  some/pkg/v2/mod1 v2.0.0
  my/pkg/v3 v3.0.1
)

格式总结为pkgpath/vN,其中N是大于1的主要版本号。在代码里导入时也需要附带上这个版本信息,如import “some/pkg/v2”。如此一来包的导入路径发生了变化,也不用担心名称相同的对象需要向后兼容的限制了,因为golang认为不同的导入路径意味着不同的包。

不过这里有几个例外可以不用参照这种写法:

  1. 当使用gopkg.in格式时可以使用等价的require gopkg.in/some/pkg.v2 v2.0.0
  2. 在版本信息后加上+incompatible就可以不需要指定/vN,例如:require some/pkg v2.0.0+incompatible
  3. 使用go1.11时设置GO111MODULE=off将取消这种限制,当然go1.12里就不能这么干了

除此以外的情况如果直接使用v2+版本将会导致go mod报错。

v2+版本的包允许和其他不同大版本的包同时存在(前提是添加了/vN),它们将被当做不同的包来处理。

另外/vN并不会影响你的仓库,不需要创建一个v2对应的仓库,这只是go modules添加的一种附加信息而已。

当然如果你不想遵循这一规范或者需要兼容现有代码,那么指定+incompatible会是一个合理的选择。不过如其字面意思,go modules不推荐这种行为。

有时候你能在 go.mod 文件中发现不兼容的标记,v3.2.1+incompatible,这是因为这个依赖包没有使用 go module,并且它通过 git 打了 tag。

go.mod 文件

初始化go.mod

首先,必须为项目选择一个名称并将其写入go.mod文件。该名称属于项目的根目录。您创建的每个新包必须位于其自己的子目录中,并且其名称必须与目录名称匹配。

进入到项目中,用

1
go mod init name

init 后面那段东西是你的包名。它会帮你创建一个go.mod文件,这个文件会被 go 工具链接管,一般来说不需要手工修改它,当你用go get之类的命令时,它会帮你修改这个文件。

go.mod:

1
module myprojectname

然后导入项目的包,如:

1
import myprojectname/stuff

包文件stuff必须位于项目stuff目录中。您可以根据需要命名这些文件。

此外,还可以创建更深入的项目结构。例如,您决定将源代码文件与其他文件分开(例如app configs,docker文件,静态文件等)。我们将stuff目录移到里面pkg,里面的每个go文件pkg/stuff都有stuff包名。要导入东西包只需写:

1
import myprojectname/pkg/stuff

没有什么可以阻止你从层次结构,如创建多个级别myprojectname/pkg/db/provider/postgresql,其中postgresql是包名和pkg/db/provider/postgresql是路径相对于项目的根包。

如果要放在github上作为modules使用,需要这样初始化 module:

1
2
$ go mod init github.com/objcoding/testmod
go: creating new go.mod: module github.com/objcoding/testmod

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

1
module github.com/objcoding/testmod

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

推送到 github 仓库

1
2
3
4
git init
git add *
git commit -am "First commit"
git push -u origin master

在这里我也着重说下关于项目依赖包引用地址的问题,这个问题虽小,但也确实很困扰人,所以必须得说一下:

go mudules 出现之前,在一个项目中有很多个包,在项目内,有些包需要依赖项目内其它包,假设项目有个包,相对于 gopath 的地址是 objcoding/mypackage,在项目内其它包引用这个包时,就可以通过以下引用:

1
import myproject/mypackage

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

1
import github.com/objcoding/myproject/mypackage

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

例如,在项目下新建目录 utils,创建一个tools.go文件:

1
2
3
4
5
6
7
package utils

import fmt

func PrintText(text string) {
    fmt.Println(text)
}

在根目录下的hello.go文件就可以 import “hello/utils” 引用utils

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import (
"hello/utils"

"github.com/astaxie/beego"
)

func main() {

    utils.PrintText("Hi")

    beego.Run()
}

但也有可能会出现依赖包地址正确但会报红的情况,这时极有可能是你在 Goland 编辑器中没有将项目设置为 go modules 项目,具体设置如下:

勾选了该选项之后,就会在 External Libraries 中出现 Go Modules 目录。

go.mod 文档语法

在目前的版本当中,go.mod 文件中主要有四个部分组成:

  • module: 本模块的路径,所有本模块下的包共享这个路径前缀
  • require: 定义所依赖的模块
  • replace: 替换 import 模块的路径(仅在当前模块为主模块时生效)
  • exclude: 排除某个模块,使之不能被 import(仅在当前模块为主模块时生效)

replace 和 exclude 只作用于当前模块的构建,它们既不会向上继承,也不会向下传递

module

用来声明当前 module,如果当前版本大于 v1 的话,还需要在尾部显式的声明 /vN。

1
2
3
module /path/to/your/mod/v2

module github.com/Xuanwo/go-mod-intro/v2

require

这是最为常用的部分,在 mod 之后可以写任意有效的、能指向一个引用的字符串,比如 Tag,Branch,Commit 或者是使用 latest 来表示引用最新的 commit。如果对应的引用刚好是一个 Tag 的话,这个字符串会被重写为对应的 tag;如果不是的话,这个字符串会被规范化为形如 v2.0.0-20180128182452-d3ae77c26ac8 这样的字符串。我们后面会发现这个字符串与底层的 mod 存储形式是相对应的。

1
2
3
4
require /your/mod tag/branch/commit

require github.com/google/go-github/v24 v24.0.1
require gopkg.in/urfave/cli.v2 v2.0.0-20180128182452-d3ae77c26ac8

replace

replace 这边的花样比较多,主要是两种,一个是与 require 类似,可以指向另外一个 repo,另一种是可以指向本地的一个目录。加了 replace 的话,go 在编译的时候就会使用对应的项目代码来替换。需要注意的是这个只作用于当前模块的构建,其他模块的 replace 对它不生效,同理,它的 replace 对其他模块也不会生效。

需要额外注意的是,如果引用一个本地路径的话,那这个目录下必须要有 go.mod 文件,这个目录可以是绝对路径,也可以是相对路径。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
replace original_name => real_name tag/branch/commit
replace original_name => local_path


//注意外部包replace成本地路径的时候 需要先go get 这个包,从require中获取v0.0.0-20190719094155-f38894a34ad4 本地路径不需要版本号
replace github.com/apache/rocketmq-client-go v0.0.0-20190719094155-f38894a34ad4 => ./rely/rocketmq-client-go

replace test.dev/common => git.example.com/bravo/common.git v0.0.0-20190520075948-958a278528f8
replace test.dev/common => ../../another-porject/common-go
replace github.com/qiniu/x => github.com/Xuanwo/qiniu_x v0.0.0-20190416044656-4dd63e731f37

exclude

这个用的比较少,主要是为了能在构建的时候排除掉特定的版本,跟 replace 一样,只能作用于当前模块的构建。

1
exclude /your/mod tag/branch/commit

go.mod格式化

我们永远不必自己运行这些命令 ( 格式化命令 ),因为它们是由其他命令调用的

但是为了完整起见,我们顺带提一下 ,对于 go.mod 和 go.sum 文件而言, go mod -fmt 相当于 go fmt ,并且 go mod -fix 做了一些聪明的事情以保持 go.mod 清洁,例如

  • 将非规范版本标识符重写为语义版本控制形式
  • 删除重复项
  • 更新依赖,排除非依赖

module query

除了通过传入package@version给go mod -requirement来精确“指示”module依赖之外,go mod还支持query表达式,比如:

1
# go mod -require='bitbucket.org/bigwhite/c@>=v1.1.0'

go mod会对query表达式做求值,得出build list使用的package c的版本:

1
2
3
4
5
6
7
# cat go.mod
module hello

require (
    bitbucket.org/bigwhite/c v1.1.0
    bitbucket.org/bigwhite/d v1.1.0 // indirect
)
1
2
3
4
5
6
7
# go build hello.go
go: downloading bitbucket.org/bigwhite/c v1.1.0
# ./hello
call C: v1.1.0
   --> call D:
    call D: v1.1.0
   --> call D end

go mod对module query进行求值的算法是“选择最接近于比较目标的版本(tagged version)”。以上面例子为例:

1
2
3
4
5
query text: >=v1.1.0

比较的目标版本为v1.1.0

比较形式:>=

因此,满足这一query的最接近于比较目标的版本(tagged version)就是v1.1.0。

如果我们给package d增加一个约束“小于v1.3.0”,我们再来看看go mod的选择:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# go mod -require='bitbucket.org/bigwhite/d@<v1.3.0'
# cat go.mod
module hello

require (
    bitbucket.org/bigwhite/c v1.1.0 // indirect
    bitbucket.org/bigwhite/d <v1.3.0
)

# go build hello.go
go: finding bitbucket.org/bigwhite/d v1.2.0
go: downloading bitbucket.org/bigwhite/d v1.2.0

# ./hello
call C: v1.1.0
   --> call D:
    call D: v1.2.0
   --> call D end

我们看到go mod选择了package d的v1.2.0版本,根据module query的求值算法,v1.2.0恰是最接近于“小于v1.3.0”的tagged version。

用下面这幅示意图来呈现这一算法更为直观一些:

replace

replace指令在顶层提供了额外的控制,go.mod用于满足在Go source或go.mod文件中找到的依赖项的实际用途,而replace在main模块以外的模块中使用指令构建主模块时忽略模块。

该replace指令允许您提供另一个导入路径,该路径可能是位于VCS(GitHub或其他位置)的另一个模块,或者是具有相对或绝对文件路径的本地文件系统。replace使用指令中的新导入路径,无需更新实际源代码中的导入路径。

replace 允许顶级模块控制用于依赖项的确切版本,例如:

1
replace example.com/some/dependency => example.com/some/dependency v1.2.3

replace 还允许使用分叉依赖,例如:

1
replace example.com/some/dependency => example.com/some/dependency-fork v1.2.3

一个示例用例是,如果您需要修复或调查依赖项中的某些内容,您可以使用本地分支并在顶层添加类似以下内容go.mod:

1
replace example.com/original/import/path => /your/forked/import/path

replace 也可用于通知go工具在多模块项目中模块的相对或绝对磁盘位置,例如:

1
replace example.com/project/foo => ../foo

注意:如果replace指令的右侧是文件系统路径,则目标必须在该位置具有文件go.mod。如果go.mod文件不存在,您可以创建一个go mod init。

通常,您可以选择=>在replace指令的左侧指定一个版本,但是如果省略它,通常它对更改不太敏感(例如,如replace上面所有示例中所做的那样)。

替换远程包路径

在国内访问 golang.org/x 的各个包都需要翻墙,你可以在go.mod中使用replace替换成github上对应的库。

1
2
3
4
5
replace (
	golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac => github.com/golang/crypto v0.0.0-20180820150726-614d502a4dac
	golang.org/x/net v0.0.0-20180821023952-922f4815f713 => github.com/golang/net v0.0.0-20180826012351-8a410e7b638d
	golang.org/x/text v0.3.0 => github.com/golang/text v0.3.0
)

如果我们是本地开发的包, 还没有远程仓库的时候, 要怎么解决本地包依赖问题呢?

我们先看一下一个最基本的mod文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
module GoRoomDemo

go 1.12

require (
    github.com/gin-gonic/gin v1.3.0
    github.com/gohouse/goroom v0.0.0-20190327052827-9ab674039336
    github.com/golang/protobuf v1.3.1 // indirect
    github.com/gomodule/redigo v2.0.0+incompatible
    github.com/mattn/go-sqlite3 v1.10.0
    github.com/stretchr/testify v1.3.0 // indirect
    golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53 // indirect
)

这是一个简单的GoRoom框架的依赖关系包, 如果我想使用本地的goroom, 只需要使用replace即可

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
module GoRoomDemo

go 1.12

require (
    github.com/gin-gonic/gin v1.3.0
    github.com/gohouse/goroom v0.0.0-20190327052827-9ab674039336
    github.com/golang/protobuf v1.3.1 // indirect
    github.com/gomodule/redigo v2.0.0+incompatible
    github.com/mattn/go-sqlite3 v1.10.0
    github.com/stretchr/testify v1.3.0 // indirect
    golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53 // indirect
)

replace github.com/gohouse/goroom => /path/to/go/src/github.com/gohouse/goroom

这里的 path/to/go/src/github.com/gohouse/goroom 是本地的包路径 这样, 我们就可以愉快的使用本地目录了

依赖库中的 replace 对你的主 go.mod 不起作用,比如 github.com/smallnest/rpcx 的 go.mod 已经增加了replace,但是你的go.mod虽然require了rpcx的库,但是没有设置replace的话, go get还是会访问 golang.org/x 。

所以如果想编译那个项目,就在哪个项目中增加replace。

顶层依赖与间接依赖

如果你因为 golang.org/x/… 无法获取而使用replace进行替换,那么你肯定遇到过问题。明明已经replace的包为何还会去未替换的地址进行搜索和下载?

解释这个问题前先看一个go.mod的例子,这个项目使用的第三方模块使用了golang.org/x/…的包,但项目中没有直接引用它们:

1
2
3
4
5
6
7
8
module schanclient

require (
    github.com/PuerkitoBio/goquery v1.4.1
    github.com/andybalholm/cascadia v1.0.0 // indirect
    github.com/chromedp/chromedp v0.1.2
    golang.org/x/net v0.0.0-20180824152047-4bcd98cce591 // indirect
)

go.mod 中只会添加直接的依赖,间接的依赖都是隐含的,下列几种特殊情况会在后面加上// indirect标记出来

  • 手动指定了更高的依赖版本,比如在不引用 golang.org/x/text 的前提下通过 go get golang.org/x/text@v0.3.2 升级依赖
  • 依赖的库还没有切换到 Go Module,这时候go工具链是不知道内部的依赖关系的,所以所有的依赖都会直接添加到当前模块中

注意github.com/andybalholm/cascadia v1.0.0和golang.org/x/net v0.0.0-20180824152047-4bcd98cce591后面的// indirect,它表示这是一个间接依赖。

间接依赖是指在当前module中没有直接import,而被当前module使用的第三方module引入的包,相对的顶层依赖就是在当前module中被直接import的包。如果二者规则发生冲突,那么顶层依赖的规则覆盖间接依赖。

在这里golang.org/x/netgithub.com/chromedp/chromedp引入,但当前项目未直接import,所以是一个间接依赖,而github.com/chromedp/chromedp被直接引入和使用,所以它是一个顶层依赖。

而我们的replace命令只能管理顶层依赖,所以在这里你使用replace golang.org/x/net => github.com/golang/net是没用的,这就是为什么会出现go build时仍然去下载golang.org/x/net的原因。

那么如果我把// indirect去掉了,那么不就变成顶层依赖了吗?答案当然是不行。不管是直接编辑还是go mod edit修改,我们为go.mod添加的信息都只是对go mod的一种提示而已,当运行go build或是go mod tidy时golang会自动更新go.mod导致某些修改无效,简单来说一个包是顶层依赖还是间接依赖,取决于它在本module中是否被直接import,而不是在go.mod文件中是否包含// indirect注释。

这样限制的原因也很好理解,因为对于包进行替换后,通常不能保证兼容性,对于一些使用了这个包的第三方module来说可能意味着潜在的缺陷,而允许顶层依赖的替换则意味着你对自己的项目有充足的自信不会因为replace引入问题,是可控的。相当符合golang的工程性原则。

本地包替换

replace除了可以将远程的包进行替换外,还可以将本地存在的modules替换成任意指定的名字。

假设我们有如下的项目:

1
2
3
4
5
6
7
8
tree my-mod

my-mod
├── go.mod
├── main.go
└── pkg
    ├── go.mod
    └── pkg.go

其中main.go负责调用my/example/pkg中的Hello函数打印一句“Hello”,my/example/pkg显然是个不存在的包,我们将用本地目录的pkg包替换它,这是main.go:

1
2
3
4
5
6
7
package main

import "my/example/pkg"

func main() {
    pkg.Hello()
}

我们的pkg.go相对来说很简单:

1
2
3
4
5
6
7
package pkg

import "fmt"

func Hello() {
    fmt.Println("Hello")
}

重点在于go.mod文件,虽然不推荐直接编辑mod文件,但在这个例子中与使用go mod edit的效果几乎没有区别,所以你可以尝试自己动手修改my-mod/go.mod:

1
2
3
4
5
module my-mod

require my/example/pkg v0.0.0

replace my/example/pkg => ./pkg

至于pkg/go.mod,使用go mod init生成后不用做任何修改,它只是让我们的pkg成为一个module,因为replace的源和目标都只能是go modules。

因为被replace的包首先需要被require,所以在my-mod/go.mod中我们需要先指定依赖的包,即使它并不存在。对于一个会被replace的包,如果是用本地的module进行替换,那么可以指定版本为v0.0.0(对于没有使用版本控制的包只能指定这个版本),否则应该和替换包的指定版本一致。

再看replace my/example/pkg => ./pkg这句,与替换远程包时一样,只是将替换用的包名改为了本地module所在的绝对或相对路径。

一切准备就绪,我们运行go build,然后项目目录会变成这样:

1
2
3
4
5
6
7
8
9
tree my-mod

my-mod
├── go.mod
├── main.go
├── my-mod
└── pkg
    ├── go.mod
    └── pkg.go

那个叫my-mod的文件就是编译好的程序,我们运行它:

1
2
./my-mod
Hello

运行成功,my/example/pkg已经替换成了本地的pkg。

同时我们注意到,使用本地包进行替换时并不会生成go.sum所需的信息,所以go.sum文件也没有生成。

本地替换的价值在于它提供了一种使自动生成的代码进入go modules系统的途径,毕竟不管是go tools还是rpc工具,这些自动生成代码也是项目的一部分,如果不能纳入包管理器的管理范围想必会带来很大的麻烦。

go.sum文件

也许你知道npm的package-lock.json的作用,它会记录所有库的准确版本,来源以及校验和,从而帮助开发者使用正确版本的包。通常我们发布时不会带上它,因为package.json已经够用,而package-lock.json的内容过于详细反而会对版本控制以及变更记录等带来负面影响。

如果看到go.sum文件的话,也许你会觉得它和package-lock.json一样也是一个锁文件,那就大错特错了。go.sum不是锁文件。

更准确地来说,go.sum是一个构建状态跟踪文件。它会记录当前module所有的顶层和间接依赖,以及这些依赖的校验和,它包含了指定的模块的版本内容的哈希值作为校验参考,从而提供一个可以100%复现的构建过程并对构建对象提供安全性的保证。

go.sum同时还会保留过去使用的包的版本信息,以便日后可能的版本回退,这一点也与普通的锁文件不同。所以go.sum并不是包管理器的锁文件。

因此我们应该把go.sum和go.mod一同添加进版本控制工具的跟踪列表,同时需要随着你的模块一起发布。如果你发布的模块中不包含此文件,使用者在构建时会报错,同时还可能出现安全风险(go.sum提供了安全性的校验)。

go get 与 go mod download

默认情况下,Go 不会自己更新模块,这是一个好事因为我们希望我们的构建是有可预见性(predictability)的。如果每次依赖的包一有更新发布,Go 的 module 就自动更新,那么我们宁愿回到 Go v1.11 之前没有 Go module 的荒莽时代了。所以,我们需要更新 module 的话,我们要显式地告诉 Go。

go mod download 与 go get 功能相同

mod开启后,go get命令的使用方式也发生了变更为获取依赖的特定版本,用来升级和降级依赖。可以自动修改 go.mod 文件,而且依赖的依赖版本号也可能会变。新版 go get 可以在末尾加 @ 符号,用来指定版本。版本号必须符合https://semver.org/lang/zh-CN/ 的规范,版本号前面需要带”v”

版本格式:主版本号.次版本号.修订号,版本号递增规则如下:

  1. 主版本号:当你做了不兼容的 API 修改,
  2. 次版本号:当你做了向下兼容的功能性新增,
  3. 修订号:当你做了向下兼容的问题修正。

先行版本号及版本编译元数据可以加到“主版本号.次版本号.修订号”的后面,作为延伸。

举例:

1
2
3
4
5
go get github.com/shawnfeng/sutil # 匹配最新的一个 tag
go get github.com/shawnfeng/sutil@latest # 和上面一样
go get github.com/shawnfeng/sutil@v1.0.5 # 匹配 v1.0.5
go get github.com/shawnfeng/sutil@5346574fa3b3 # 匹配 5346574fa3b3 版本
go get github.com/shawnfeng/sutil@master # 匹配 master 分支

包的安装模式也是被允许的,比如使用go get golang.org/x/perf/cmd/..来更新cmd下的所有子包。

查看依赖包

1
2
go list -m all	        列出当前模块依赖的所有模块
go list -u -m all	列出当前模块依赖中可升级的模块

可以直接查看 go.mod 文件,或者使用命令行:

1
2
3
4
5
6
7
$ go list -m all
github.com/adesight/test
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a
golang.org/x/text v0.3.0
rsc.io/quote v1.5.2
rsc.io/sampler v1.99.99
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ go list -m -json all # json 格式输出
{
        "Path": "golang.org/x/text",
        "Version": "v0.3.0",
        "Time": "2017-12-14T13:08:43Z",
        "Indirect": true,
        "Dir": "/Users/lishude/go/pkg/mod/golang.org/x/text@v0.3.0",
        "GoMod": "/Users/lishude/go/pkg/mod/cache/download/golang.org/x/text/@v/v0.3.0.mod"
}
{
        "Path": "rsc.io/quote",
        "Version": "v1.5.2",
        "Time": "2018-02-14T15:44:20Z",
        "Dir": "/Users/lishude/go/pkg/mod/rsc.io/quote@v1.5.2",
        "GoMod": "/Users/lishude/go/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.mod"
}

依赖包在 $GOPATH/pkg/mod 目录中:

那么,新的结构到底是什么样的呢?假设我们正在开发的项目依赖于 github.com/me/lib 且版本号 1.0.0 的模块,对于这种情况,我们会发现在 GOPATH/src/mod 中文件结构如下

从上面这张图中我们可以看到

  • 依赖项的源代码文件结构保存在此目录的根目录,并做了一些细微改动:导入路径以 @version 为后缀
  • 从 VCS 中获取或构建的源归档文件放置在 download 目录中
  • VCS 数据保存在 vcs 目录中

添加/指定依赖

通过go get命令来添加依赖,依赖的包及其版本会被记录在go.mod,包名、版本及哈希值会被记录到go.sum这个文件中。注意,这个go.sum并不是一个 lock 文件。

  • 添加依赖: go get github.com/gorilla/mux
  • 添加特定版本: go get github.com/gorilla/mux@v1.6.2
  • 添加特定版本,限定到版本范围: go get github.com/gorilla/mux@’<v1.6.2'
  • 添加特定版本,限定到特定 git commit: go get github.com/gorilla/mux@e3702bed2

需要特别注意的是,gomod 除了遵循语义化版本原则外,还遵循最小版本选择原则,也就是说如果当前版本是 v1.1.0,只会下载不超过这个最大版本号。如果使用 go get foo@master,下次再下载只会和第一次的一样,无论 master 分支是否更新了代码,如下所示,使用包含当前最新提交哈希的虚拟版本号替代直接的 master 版本号。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ go get golang.org/x/crypto/sha3@master
go: finding golang.org/x/crypto/sha3 latest
go: finding golang.org/x/crypto latest
$ cat go.mod
module github.com/adesight/test

go 1.12

require (
	golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a // indirect
	rsc.io/quote v1.5.2
)

最小版本选择

最小版本选择的工作方式是这样的:我们为每个模块指定的依赖都是可用于构建的最低版本,最后实际选择的版本是所有出现过的最低版本中的最大值。

我们现在有这样的一个依赖关系,A 会依赖 B,C,而 B,C 会去依赖 D,D 又会依赖 E。

那么我们从 A 开始做一个 BFS (仅用于讲解原理,背后实现不一定是这样) ,把每个模块依赖的版本都找出来,这样我们会首先得到一个粗略的清单。然后相同的模块我们总是取最大的版本,这样就能得到最终的依赖列表。

为什么可以这样呢?

  • 导入兼容性规则 规定了相同的导入路径,新包必须向后兼容旧包,因此只要 D 还是 v1 版本,不管是选择 v1.3 还是 v1.4 都是可以的,不会有破坏性的变更。
  • 语义导入版本控制 规定了不同的大版本需要使用不同的导入路径,因此假设 D 升级到了 v2 版本,那就应当选择 D v1.4 和 D v2.0 这两个包了。

为什么要这样做呢?

为了可重现构建,为了降低复杂度。

大多数包管理工具,包括 dep,cargo 和 pip 等,采用的都是总是选择允许的最新版本(use the newest allowed version)策略。这会带来两个问题:第一,允许的最新版本可能会随着外部事件而发生变化,比如说在构建的时候,依赖的一个库刚好发布了一个新版本,这会导致可重现构建失效;第二,开发者为了避免依赖在构建期间发生变化,他必须显式的告诉依赖管理工具我不要哪些版本,比如:>= 0.3, <= 0.4。这会导致依赖管理工具花费大量的时间去计算可用的版本,而最终的结果总是让人感到沮丧,A 依赖需要 Z >= 0.5 而 B 依赖需要 Z <= 0.4.

与总是选择允许的最新版本相反,Go Module 默认采用的是总是使用允许的最旧的版本。我们在 go.mod 中描述的 vX.Y.Z 实际上是在告诉编译器:“Hey,我最少需要 vX.Y.Z 才能被 Build 出来”,编译器听完了所有模块的话之后按照刚才描述的流程就能选择出允许的最旧的那个版本。

升级依赖

升级次级或补丁版本号:

1
go get -u rsc.io/quote

仅升级补丁版本号:

1
go get -u=patch rscio/quote

升降级版本号,可以使用比较运算符控制:

1
go get foo@'<v1.6.2'

补全/清理依赖

当前代码中不需要了某些包,删除相关代码片段后并没有在 go.mod 文件中自动移出。

运行下面命令可以移出所有代码中不需要的包:

1
go mod tidy

如果仅仅修改 go.mod 配置文件的内容,那么可以运行 go mod edit --droprequire=path,比如要移出 golang.org/x/crypto

1
go mod edit --droprequire=golang.org/x/crypto

依赖缓存

$GOPATH/pkg/mod/cache/download/ 中有原始代码的缓存,避免重复下载:

1
2
3
4
5
$ ls $GOPATH/pkg/mod/cache/download/github.com/lijiaocn
codes-go golib

$ ls $GOPATH/pkg/mod/cache/download/github.com/lijiaocn/golib/@v
list           list.lock      v0.0.1.info    v0.0.1.lock    v0.0.1.mod     v0.0.1.zip     v0.0.1.ziphash

go mod 会在本地缓存代码,如果被引用的代码的版本号不变,但是代码变了(在做实验或者代码版本管理比较乱的时候,可能会出现的这种情况),清除本地缓存( $GOPATH/pkg/mod/cache$GOPATH/pkg/mod/ 依赖代码 )后,才能重新拉取最新的代码(可能会有其它的更新缓存的方法);

vendor

golang一直提供了工具选择上的自由性,如果你不喜欢go mod的缓存方式,你可以使用go mod vendor回到godep或govendor使用的vendor目录进行包管理的方式。

当然这个命令并不能让你从godep之类的工具迁移到go modules,它只是单纯地把go.sum中的所有依赖下载到vendor目录里,如果你用它迁移godep你会发现vendor目录里的包回合godep指定的产生相当大的差异,所以请务必不要这样做。

我们举第一部分中用到的项目做例子,使用go mod vendor之后项目结构是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
tree my-module

my-module
├── go.mod
├── go.sum
├── main.go
└── vendor
    ├── github.com
    │   ├── mattn
    │   │   └── go-gtk
    │   │       └── glib
    │   │           ├── glib.go
    │   │           └── glib.go.h
    │   └── mqu
    │       └── go-notify
    │           ├── LICENSE
    │           ├── README
    │           └── notify.go
    └── modules.txt

可以看到依赖被放入了vendor目录。

接下来使用go build -mod=vendor来构建项目,因为在go modules模式下go build是屏蔽vendor机制的,所以需要特定参数重新开启vendor机制:

1
2
3
go build -mod=vendor
./my-module
a notify!

构建成功。当发布时也只需要和使用godep时一样将vendor目录带上即可。

我猜想大多数要使用 vendor 机制的开发者,在他们自己的开发机器上会使用 go build ,而在他们的CI系统(Continuous Integration,持续集成)上则使用 -mod vendor 选项

还有,对于那些不想要直接依赖版本控制服务(译注:比如 github.com)上游代码的人来说,比起用 vendor 这种机制,更好的方法是使用 Go module 代理。

有很多方法可以保证 go 不会联网去获取包代码(比如 GOPROXY=off),但这些内容只能在之后的文章提及了。

可以通过设置GOFLAGS=-mod=vendor环境变量来保持选择vendor,此时go get无法正常使用.

发布新版本

  1. 修改 go.mod 第一行,在module那行最后加上/v2:go mod edit --module=github.com/islishude/gomodtest/v2

  2. 对于不兼容的改动(除了 v0 和 v1),都必须显示得修改 import 的路径。所以我们的引用需要改成 import "github.com/mnhkahn/aaa/v2/config。在所有的地方都需要修改,包括自己的包内和调用方包。

  3. 代码提交之后需要打新 tag,v2.0.0。

    1
    2
    
    git tag v1.0.0 //注意 这个格式是Go的规定,格式为 v(major).(minor).(patch)
    git push --tags //推送 tag
    

这时我们最好还需要创建一条 v1 分支,以便我们在其它分支写代码不会影响到 v1.0.0 版本:

1
2
git checkout -b v1
git push -u origin v1

更新依赖主版本

根据上面的说明,想必你会看到一个问题,当我们升级主版本号的时候,要更改 module 名称,也就是上面所说的加上版本号,这就存在一个问题,如果我们要更新到主版本号的依赖就没有这么简单了,因为升级的依赖包路径都需要修改,这个在其它语言包管理以及 Go 第三方包管理工具都不存在的一点。

需要同时使用一个代码库,版本冲突的情况

看如下代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package modtest
import (
  "fmt"
  "github.com/Aruforce/hello" // v1版本
  hv2 "github.com/Aruforce/hello/v2"
  //v2对go来说相当于一个全新的依赖了,对于这种模式的路径golang赋予的别名还是hello就原版本有名字上的冲突,需要特别声明,也就是一种冲突解决方法
  // 进行go get等等时,会下载 v2.x.x的代码
)
func Mod() string{
  fmt.Println(hv2.Hello())
  return hello.Hello();
}

直接大版本升级的情况

看如下代码:

1
2
3
4
5
6
7
8
9
package modtest
import (
  "fmt"
  "github.com/Aruforce/hello/v2"//v2对go来说相当于一个全新的依赖了,对于这种模式的路径golang赋予的别名还是hello,这就造成一个情况,原本自己的代码完全不需要修改
  //go get 等等时,会下载v2.x.x最大版号的代码
)
func Mod() string{
  return hello.Hello();
}

和我们没有关系的情况

看如下代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package modtest
import (
  "fmt"
  "github.com/Aruforce/hello" // v1版本
  //这种情况下,小版本patch更新,只要我们不手动 go get -u 本地代码就不会影响
  //大版本更新,即使go get -u 也不会更新到大版本,除非显示生命
  //其余就是使用你指定的新版本
)
func Mod() string{
  return hello.Hello();
}

实例演示

实现一个用 go modules 管理的 package: github.com/introclass/go_mod_example_pkg

在另一个使用 go modules 的项目中引用 v1.0.1 版本:github.com/introclass/

1
2
3
4
$ go get github.com/introclass/go_mod_example_pkg@v1.0.1
go: finding github.com/introclass/go_mod_example_pkg v1.0.1
go: downloading github.com/introclass/go_mod_example_pkg v1.0.1
go: extracting github.com/introclass/go_mod_example_pkg v1.0.1

查看依赖的代码,显示依赖的是 v1.0.1:

1
2
3
4
$ go list  -m all
example.com/hello
github.com/introclass/go_mod_example_pkg v1.0.1
github.com/lijiaocn/golib v2.0.1+incompatible

在 main 函数中使用导入的依赖包:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import (
    "example.com/hello/display"
    pkg "github.com/introclass/go_mod_example_pkg"
    "github.com/lijiaocn/golib/version"
)

func main() {
    version.Show()
    display.Display("display print\n")
    pkg.Vesrion()
}

编译执行,输出的v1.0.1:

1
2
3
4
$ ./hello
version:    compile at:   golib v2
display print
v1.0.1

将依赖包切换到版本 2.0.1:

1
2
$ go get github.com/introclass/go_mod_example_pkg@v2.0.1
go: finding github.com/introclass/go_mod_example_pkg v2.0.1

重新编译执行,输出的版本是 v2.0.1:

1
2
3
4
$ ./hello
version:    compile at:   golib v2
display print
v2.0.1

引用依赖包 v3.0.1 版本的 v3 子目录(事实上是一个独立的 pacakge ):

1
2
3
4
$ go get github.com/introclass/go_mod_example_pkg/v3@v3.0.1
go: finding github.com/introclass/go_mod_example_pkg/v3 v3.0.1
go: downloading github.com/introclass/go_mod_example_pkg/v3 v3.0.1
go: extracting github.com/introclass/go_mod_example_pkg/v3 v3.0.1

修改 main 函数,引用 v3:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
    "example.com/hello/display"
    pkg "github.com/introclass/go_mod_example_pkg"
    pkgv3 "github.com/introclass/go_mod_example_pkg/v3"
    "github.com/lijiaocn/golib/version"
)

func main() {
    version.Show()
    display.Display("display print\n")
    pkg.Vesrion()
    pkgv3.Vesrion()
}

重新编译执行,分别输出 v2.0.1 和 v3.0.1 in v3:

1
2
3
4
5
$ ./hello
version:    compile at:   golib v2
display print
v2.0.1
v3.0.1 in v3

go build

  • go build -mod=readonly 防止隐式修改 go.mod,如果遇到有隐式修改的情况会报错,可以用来测试 go.mod 中的依赖是否整洁,但如果明确调用了 go mod、go get 命令则依然会导致 go.mod 文件被修改。

  • go build -mod=vendor 在开启模块支持的情况下,用这个可以退回到使用 vendor 的时代。

Go module proxy

go get命令默认情况下,无论是在gopath mode还是module-aware mode,都是直接从vcs服务(比如github、gitlab等)下载module的。但是Go 1.11中,我们可以通过设置GOPROXY环境变量来做一些改变:让Go命令从其他地方下载module。比如:

1
export GOPROXY=https://goproxy.io

一旦如上面设置生效后,后续go命令会通过go module download protocol与proxy交互下载特定版本的module。

参考: https://www.cnblogs.com/apocelipes/p/10295096.html https://zhuanlan.zhihu.com/p/59687626 https://blog.cyeam.com/go/2019/03/12/go-version https://my.oschina.net/Aruforce/blog/3043573 https://studygolang.com/articles/19334 https://www.twle.cn/t/145 https://xuanwo.io/2019/05/27/go-modules/ https://tonybai.com/2018/07/15/hello-go-module/