1 速览
在正式了解Golang Modules之前,我们先速览一下其使用方式。
在$GOPATH
之外的任意地方,创建一个文件夹:
$ mkdir -p /tmp/hello
$ cd /tmp/hello
然后初始化一个新的Module:
$ go mod init github.com/leileiluoluo/hello
输出:
go: creating new go.mod: module github.com/leileiluoluo/hello
go.mod内容为:
module github.com/leileiluoluo/hello
go 1.12
然后写一段代码:
$ cat < hello.go
package main
import (
"fmt"
"github.com/leileiluoluo/quote"
)
func main() {
fmt.Println(quote.Hello())
}
EOF
build一下:
$ go build
go: finding github.com/leileiluoluo/quote latest
go: downloading github.com/leileiluoluo/quote v0.0.0-20190510033103-5cb7d4598cfa
go: extracting github.com/leileiluoluo/quote v0.0.0-20190510033103-5cb7d4598cfa
go.mod内容为:
module github.com/leileiluoluo/hello
go 1.12
require github.com/leileiluoluo/quote v0.0.0-20190510033103-5cb7d4598cfa
可以看到,其会从https://github.com/leileiluoluo/quote
的master
分支拉取最新提交5cb7d4598cfa
。
该依赖工程非Module管理模式,其仅有两个文件:
hello.go
README.md
现在给依赖工程打一个TAG,名为v1.0.0
。然后hello工程更新依赖:
$ go get -u
go: finding github.com/leileiluoluo/quote v1.0.0
go: downloading github.com/leileiluoluo/quote v1.0.0
go: extracting github.com/leileiluoluo/quote v1.0.0
查看go.mod内容为:
module github.com/leileiluoluo/hello
go 1.12
require github.com/leileiluoluo/quote v1.0.0
如下为使用Golang Module后的日常工作流。
每日工作流:
a)源码根据需要加入包引入语句;
b)标准命令,如go build
及go test
等会自动更新go.mod并下载依赖包;
c)当需要特定版本时,可以使用诸如go get foo@v1.2.3
,go get foo@master
,go get foo@e3702bed2
命令或直接编辑go.mod文件。
其它通用命令:
a)go list -m all
查看一次构建使用的直接及间接依赖的最终版本;
b)go list -u -m all
查看直接及间接依赖的可用的小版本或补丁版本更新;
c)go get -u
或 go get -u=patch
将直接与间接依赖更新为最新小版本或补丁版本;
d)go build ./...
或 go test ./...
构建或测试模块中的所有包;
e)go mod tidy
从go.mod清理不再使用的包;
f)go mod edit -replace foo@v1.2=../foo
替换依赖为本地复制或指定版本;
g)go mod vendor
转换为vendor依赖方式。
2 概念
仓库、模块与包的关系:
a)一个仓库包含一个或多个模块;
b)一个模块包含一个或多个包;
c)一个包在一个文件夹包含一个或多个.go文件。
模块必须以语义学版本命名,格式为v(主版本).(小版本).(补丁),诸如v0.1.0
、v1.2.3
,v1.5.0-rc.1
等。
模块由一组源文件树在根目录定义一个go.mod文件,模块源码可以位于GOPATH
之外,有如下原语module
,require
,replace
,exclude
。
如下为github.com/leileiluoluo/hello
模块的go.mod文件示例内容:
module github.com/leileiluoluo/hello
require (
github.com/some/dependency v1.2.3
github.com/another/dependency/v4 v4.0.0
)
可以看到,一个模块通过module原语声明模块ID,其标识模块路径。该模块下某一个包的被引用路径由该模块路径与自go.mod所在路径起一直到包的路径止的相对路径共同决定。如,一个模块在go.mod声明其ID为example.com/my/module
,那么引用该模块下mypkg
包的代码为:
import "example.com/my/module/mypkg"
go build
,go test
命令会自动找到对应的包并在go.mod中采用require原语加入该直接依赖的 最高版本。例如,您依赖的模块M的带标签的发布版本为v1.2.3
,那么您的go.mod
会新加入require M v1.2.3
这一行语句,意味着依赖模块M所允许的版本>= v1.2.3
并< v2
(v2被认为与v1不兼容)。 最小版本选择算法用于对一次构建的所有模块选择版本,对于每个模块,采用该算法选择的版本为语义学最高版本。
下面举个例子:
若您依赖的模块A依赖D(require D v1.0.0
),而您依赖的模块B同样依赖D(require D v1.1.1
),然后,最小版本选择(选择最高的版本)将会选择v1.1.1
版本的D。而选择v1.1.1
版本的D是一致的,即使将来发布了v1.2.0
版本的D。这样即可保持100%可重复构建。当然您也可以手动升级D为最新可用版本或者指定其为其它版本。查看所选模块版本列表(包括间接依赖),可以使用:
go list -m all
开放使用的包在演进时应保持向后兼容的准则,Go 1兼容性准则即是一个好的参考。不要移除已导出的名称,若要加一个新功能,需加一个新接口,不要改动老接口名。实在需要推倒之前的,请创建一个新包,以新的路径而被引用。
最后一句很重要,若破坏了兼容性,需更改包的引用路径。对Go 1.11模块而言,引用兼容性准则可以概述为:
若新包沿用旧包的引用路径,新包必须向后兼容旧包。
参考语义学版本命名规则,当一个原始为v1或v1以上的包发生了不兼容变更,该包需要更改主版本。所以,根据引用兼容性准则及语义学版本命名规则(合称为语义学版本引用),主版本需要包含在引用路径内。这样即可保障不兼容的主版本升级时,引用路径即会改变。根据语义学版本引用规则,选用Go Module的代码必须遵守如下规则:
a)语义学版本命名;
b)若一个Module的版本为v2
及以上,模块的主版本必须包含在模块路径及引用路径中(例如,声明方:module github.com/my/mod/v2
,引用方:require github.com/my/mod/v2 v2.0.0
,包引用处:import "github.com/my/mod/v2/mypkg"
);
c)例外,若模块主版本为v0
或v1
,模块路径及引用路径无须包含主版本。
通常来讲,引用路径不同的包是两个全然不同的包(如math/rand
和crypto/rand
是两个不同的包)。同样,包含不同主版本的引用路径所标识的包亦是两个不同的包。因此,example.com/my/mod/mypkg
与example.com/my/mod/v2/mypkg
是不同的包,且可能会在一次构建中同时引用。因有些模块还未转换为Module方式,过度期,会支持如下几个例外:
会继续支持gopkg.in/yaml.v1
或gopkg.in/yaml.v2
等引用方式。
b)当引用还未Module化的v2+
版本包时,会有’+incompatible
‘后缀。
c)当Module模式未开启时,采用最小模块兼容性。
即在Go 1.11邻近版本,不开启Module模式时(GO111MODULE=off
),引用v2或以上版本,不会将版本加入路径中。
3 使用
a)在$GOPATH/src
文件夹之外使用go命令,且当前文件夹或其上层文件夹包含go.mod文件,而GO111MODULE``环境变量未设置或设置为了auto
;
b)设置GO111MODULE=on
,然后调用go命令。
即在$GOPATH/src之外使用模块支持,无需设置GO111MODULE
环境变量,而在$GOPATH/src
使用模块支持,需将GO111MODULE
设置为on
。
$ cd path
该文件夹可以为设置GO111MODULE=on
的$GOPATH/src
,或该文件夹之外的任意路径。
$ go mod init github.com/my/repo
若在初始化一个v2+的模块,需要手动更改go.mod文件及.go代码,以在引用路径及模块路径加入版本信息(语义学版本引用)。
$ go build ./...
“./...
”模式匹配了当前模块下的所有包,go build将自动增加缺失的包。
$ go test ./...
或者执行如下语句,可以运行模块内的测试及所有直接及间接依赖测试以检查不兼容问题。
$ go test all
go get
命令进行日常依赖升级及降级,其会自动更新go.mod
文件,当然您也可以手动编辑go.mod
文件。当然,go get
也如go build
,go test
一样,会自动加入缺失的依赖包。查看可用的小版本或补丁更新,可以执行:$ go list -u -m all
将直接或间接依赖更新为最新的小版本或补丁版本,可以执行:
$ go get -u
仅更新为补丁版本,可以执行:
$ go get -u=patch
go get foo
等同于go get foo@latest
,会将foo
更新为最新版本。当有语义学版本时,最新版本为语义学最新版本,没有时,为最新的提交。一个通常错误的认为是,go get -u foo
仅获取最新版本的foo
。其实其还会获取foo
的直接或间接依赖的最新版本。更新版本,推荐的做法是先运行go get foo
,好使时再运行go get -u foo
。
进行版本升降级时,可以使用@version
后缀,如:
$ go get foo@v1.6.2
或
$ go get foo@e3702bed2
还支持模块查询,如:
$ go get foo@'<v1.6.2'
使用分支名称,可以不考虑其是否有语义学版本,而更新为最新的分支提交。
$ go get foo@master
版本升降级后,需测试是否有不兼容问题:
$ go test all
go mod tidy
然后执行如下命令,保证兼容性。
go test all
然后发布时,需将go.sum文件与go.mod一起提交。
发布v2及以上版本时需注意满足语义学版本引用规则,版本需包含在模块路径及引用路径中。创建一个v2及以上的版本,有如下两种方式:
go.mod文件包含vN路径(如:module github.com/my/module/v3
),模块内的包引用亦需修改为包含版本的格式(如:import "github.com/my/module/v3/mypkg"
)。
创建vN子文件夹,且将go.mod放至该文件夹下,模块路径需以/vN结尾,然后将代码拷贝至vN子文件夹下,然后更新模块内的包引用路径(如:import "github.com/my/module/v3/mypkg"
)。
最后,创建一个满足语义学版本的tag,推送至仓库即可。
但需注意子模块的情形,该种情形tag需包含前缀。
如,我们有模块example.com/repo/sub/v2
,然后想发布版本v2.1.6
,仓库为example.com/repo
,子模块定义在sub/v2/go.mod
,提交时tag需命名为sub/v2.1.6
。
参考资料