本文是Go语言系列文章Golang Notes的其中一篇,完整的文章列表请去总章查看。
后续模块
相关技术文字内容主要参考自官方wiki说明:Go 1.11 Modules。强烈建议有时间的可以自己通读一遍,任何第三方的讲解都不可能比这篇原文更详细、更细节。这是我见过的最啰嗦
的技术说明文章之一。
UPDATE 2019-03-21
官方博客3.19姗姗来迟了一篇模块博文,算是补足了正规渠道的引导:The Go Blog > Using Go Modules。比起之前的wiki,这篇博客算是做了点总结,不像wiki那么啰嗦了。
Go语言的包管理可以说是黑历史了,1.5之前基本上没有解决方案,1.5开始出了个vendor文件夹
,后面几个版本大部分的工具都围绕着这个文件夹做文章。最后终于到了1.11版本才出了个官方的modules解决方案(vgo),算是尘埃落定。估计一开始做语言设计的时候,大佬觉得包管理根本没必要,怎么随意怎么来。结果后面大厂们都开始用go语言构建大型系统和生态了,才慢慢暴露出问题,文件下载随便一扔的解决方案太过随意,有太多太多的不安定要素。
我之前一直对go语言持观望态度(即便是这两年最火的时候),就是因为这个包管理的原因。于是到了1.11,我终于开始觉得可以入手了。
1.11正式release的module这个功能应该说是稳定了,后面理论上不会再有很大的改动。这从官方给的modules wiki就可以看得出来,洋洋洒洒长篇累牍写了那么长一篇文章。
当然以目前的现状来说,仍旧远不能称完善,这里可以简单展望一下:
这些问题官方应该都是有意识到的,具体可以看这篇官方的博客:Go Modules in 2019
除了github那篇wiki,go语言的官网上并没有针对modules的详细文档(当然从内容上来说那篇也够了)。倒是命令行工具的help里有点内容(因内容过长,这里就不贴了,有兴趣的可以直接用命令查看,建议用> ~/Downloads/xxx.txt
方式输出查看):
正常使用modules的情况下,其实你并不需要特别做什么,一般就:
和modules功能出来之前没有任何不同,所有的模块管理工作在go的一系列命令中已经默认完成了。比如说go build
或go test
,等等,在执行这些命令的时候会自动对代码进行分析,找到缺失的依赖下载并将信息写入到对应的go.mod文件中。
这个环境变量用来控制是否启用modules功能,还是按之前的套路来处理go的包管理。一般来说都用这个版本了,没什么理由不把这个打开(export GO111MODULE=on
)。如果你的代码是放在GOPATH之外的,那么默认就是打开的,放在GOPATH里的,默认是关闭的(我试下来是如此)。
初始化代码模块,本质上就是创建一个go.mod文件。关于go.mod,可以看这里。
获取或更新代码,同时会更新对应的go.mod文件。对modules来说,这个命令更多用在更新依赖的版本。
打印模块依赖图,也就是打印出模块之间的依赖关系。
将依赖下载到本地cache。这个命令的应用场景一般是一个成熟项目,go.mod文件已经编辑好了,协同工作的成员可以在拉下代码之后直接用这个命令进行下载,然后就可以开始工作了。
添加缺失的依赖,并移除没有使用的依赖。算是一个清理工具,一般在做版本的时候比较常用。
如果你在使用go mod init
命令创建好一个干净空的go.mod文件之后,使用的是go build
命令来进行对应的包解析和下载的话(同时go.mod文件的内容也生成了),之后你再使用go mod tidy
来进行清理会发现:多出来很多之前go build
命令没有放进go.mod文件的包。
这里官方有一个解释:Why does ‘go mod tidy’ record indirect and test dependencies in my ‘go.mod’?
另,其他命令,例如’go build’和’go test’不会从go.mod移除依赖,即便是那些不再被需要的依赖。实质上会在go.mod里进行删除操作的,就只有go mod tidy
这个命令。
一般来说,一个go项目就是一个github代码仓库。go语言的最佳实践是一个包一个文件夹,包名和文件夹名重合。所以这里其实就有一个约定,定死的,github仓库的根目录必须
是代码的根目录。
github.com/my/repo/package_name/code.go
=>
/Users/xxx/Codes/Golang/repo/package_name/code.goimport “github.com/my/repo/package_name”
如果不是这么放,下载下来的代码文件和代码中的import路径就不一致了,会导致找不到代码。
你不可以自己映射一个文件夹作为代码的根节点,比如说:/Users/xxx/Codes/Golang/repo/src
/package_name/code.go。对于有强迫症的人来说,这还蛮恶心的。
几个例子可以看下(gin特别极端,所有源码就散落在根目录下,只有一个包,叫gin):
如果你要在代码仓库 github.com/my/repo 创建一个包含两个包(如下列举)的模块:
那么你的go.mod文件的第一行将会定义一个模块,命名为:github.com/my/repo,而磁盘上的文件结构如下:
repo/
├── go.mod
├── bar
│ └── bar.go
└── foo
└── foo.go
在之前我们已经说到,modules的出现并没有天翻地覆改变我们日常工作使用的命令和流程,但我们仍旧需要了解modules的很多细节,才能保证不犯错。这一节关于版本号相关的知识非常细节,也相当重要。
对于tag的命名,Go官方有明确的要求,不可以按自己的想法随便写。值得注意的点只有一点就是三位的版本号之前,必须添加一个v
,也就是说,所有的版本号都应该是:v1.2.3
这样的,而不是
1.2.3
这样。
看两个例子,打开链接之后观察下他们的版本号列表:
wiki原文可以查看:Version Selection
主要内容是说明在使用一些go命令,比如说’go build’、‘go test’的时候,这些命令会怎么选择依赖的版本号。这里需要关注的是,如果有多个依赖对某个依赖都有版本要求,且版本号并不一致,这种情况下会发生什么:
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 some time 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.
值得注意的是原文wiki中有一句:
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)
我没理解这是什么意思,后续需要尝试。猜测:
wiki原文可以查看:Semantic Import Versioning
使用过modules之后,会发现某些依赖的引入的最后带上了.vN
这样的字符,这里有点细节可以看下:
major版本是v2或更高的,在require的时候都需要在路径最后带上.vN
(官方给的例子里是/vN
,但我实际看下来几个包都是.vN
,先按实际例子来吧),比如说:
此外,在使用的时候(import):example.com/my/mod/mypkg
和example.com/my/mod.v2/mypkg
被认为是两个不同的包,会分开下载安装,且不会影响最后的编译。
日常中对依赖进行版本升级或降级,应该使用go get
命令:
go get dep
将依赖升级到最新版本(等同于go get dep@latest
)go get -u dep
将依赖升级到最新的minor或者patch版本go get -u=patch dep
将依赖升级到最新的patch版本go get [email protected]
将依赖升级(或降级)到指定版本注意:使用go get -u
或go get -u=patch
将会一并更新dep
的所有直接和间接依赖。最佳实践是先不要带上-u
参数运行go get
,更新完成没有问题之后,再带上-u=patch
,然后-u
,逐步更新。更新依赖的时候也最好一个一个更新,便于排查。
此外,同样会对go.mod文件进行更改的一系列命令,比如:‘go build’、‘go test’,或者是’go list’,会自动添加新需要的依赖进入go.mod来满足依赖(更新go.mod文件,并下载对应的新依赖)。
go list -u -m all
go list -m -versions $depname
这部分,更详细的可以参见:How to Upgrade and Downgrade Dependencies
官方wiki里关于go.mod的说明可以看这里:go.mod。
go.mod文件里的指令有:
注意:exclude和replace指令只在主模块的go.mod文件中生效,在依赖中则会被忽略。
比如说依赖库里用到了 golang.org/x 里的包,你因为网络关系,在你自己的go.mod将这个库replace掉了,但下载完成之后你会发现在cache里还是会有golang.org/x的代码文件,因为第三方依赖可能用到它们了。
在go.mod文件中,用得比较频繁的指令一般就只有require和replace。require不用多说,非常简单,replace还是有点细节的。
一般的使用场景:
replace是一个指令,所以它是和require同等级别,相同的使用方法,并不是要和require组合起来使用的,单独使用即可。replace指令引入的包也会直接下载。
可以试验下下面的例子:
require (
...
)
replace golang.org/x/net => github.com/golang/net v0.0.0-20190301231341-16b79f2e4e95
package main
import (
"fmt"
"golang.org/x/net/html"
"strings"
)
func main() {
z, errp := html.Parse(strings.NewReader("<html></html>"))
if errp != nil {
fmt.Println(errp)
}
fmt.Println(z)
}
如果定义一个模块名为test-module
(一般不会这么干,都是以代码仓库为模块名,e.g github.com/gin-gonic/gin
)。
则:
module test-module
$GOPATH/src/test-module
下test-module/lib/dao
package dao
import "test-module/lib/dao"
$GOPATH/src/test-module/src/lib/dao
$GOPATH/src/test-module/dao/lib/dao
$GOPATH/src/test-module/lib/dao
这也是为什么我的分布式实践项目代码库不能作为一个go模块来使用,因为我的实践代码库中,源代码相关内容并不是存放在根目录下面的。
dist-system-practice /-
| ... # 其他大量资料
| golang /- # 自定义的$GOPATH
| src /-
| dist-system-practice # 这里才是真正的项目代码
而要作为一个go模块来使用,则文件结构只能是:
dist-system-practice /-
| # 这里直接就是源码了
| lib
| vendors
| web
| ...
代码直接放在代码库根目录下。
EOF