Table of Contents
- 1. 前言
- 2. 历史 & 展望
- 3. 官方文档
- 4. 使用 & 常用命令
- 5. 模块定义 & 文件结构
- 6. 版本定义 & 更新
- 7. 补充:go.mod & replace指令
- 8. 实际经验
- 资料
1. 前言
本文是Go语言系列文章Golang Notes的其中一篇,完整的文章列表请去总章查看。
后续模块
相关技术文字内容主要参考自官方wiki说明:Go 1.11 Modules。强烈建议有时间的可以自己通读一遍,任何第三方的讲解都不可能比这篇原文更详细、更细节。这是我见过的最啰嗦
的技术说明文章之一。
UPDATE 2019-03-21
官方博客3.19姗姗来迟了一篇模块博文,算是补足了正规渠道的引导:The Go Blog > Using Go Modules。比起之前的wiki,这篇博客算是做了点总结,不像wiki那么啰嗦了。
2. 历史 & 展望
Go语言的包管理可以说是黑历史了,1.5之前基本上没有解决方案,1.5开始出了个vendor文件夹
,后面几个版本大部分的工具都围绕着这个文件夹做文章。最后终于到了1.11版本才出了个官方的modules解决方案(vgo),算是尘埃落定。估计一开始做语言设计的时候,大佬觉得包管理根本没必要,怎么随意怎么来。结果后面大厂们都开始用go语言构建大型系统和生态了,才慢慢暴露出问题,文件下载随便一扔的解决方案太过随意,有太多太多的不安定要素。
我之前一直对go语言持观望态度(即便是这两年最火的时候),就是因为这个包管理的原因。于是到了1.11,我终于开始觉得可以入手了。
1.11正式release的module这个功能应该说是稳定了,后面理论上不会再有很大的改动。这从官方给的modules wiki就可以看得出来,洋洋洒洒长篇累牍写了那么长一篇文章。
当然以目前的现状来说,仍旧远不能称完善,这里可以简单展望一下:
- 包搜索过于困难:没有一个服务能根据需求功能进行包搜索(类似www.npmjs.org)
- 包有可能不存在:这问题之前npm遇到过,发布者将库删了,结果依赖它的一堆库都挂了。npm的解决方案比较野蛮,所有上了npm的包不允许删除,永远会留在里面,就杜绝了库消失的问题。go在这方面更弱,很多东西都是一个地址,要是github库被作者删了,或者转private了,马上就出问题,而且还没人管得着
这些问题官方应该都是有意识到的,具体可以看这篇官方的博客:Go Modules in 2019
3. 官方文档
除了github那篇wiki,go语言的官网上并没有针对modules的详细文档(当然从内容上来说那篇也够了)。倒是命令行工具的help里有点内容(因内容过长,这里就不贴了,有兴趣的可以直接用命令查看,建议用> ~/Downloads/xxx.txt
方式输出查看):
- go help go.mod
- go help modules
- go help module-get
4. 使用 & 常用命令
正常使用modules的情况下,其实你并不需要特别做什么,一般就:
- 使用go mod init创建模块定义文件
- 使用go get命令下载对应的依赖(或是更新对应的依赖)
- 使用go build / go install命令来进行编译
和modules功能出来之前没有任何不同,所有的模块管理工作在go的一系列命令中已经默认完成了。比如说go build
或go test
,等等,在执行这些命令的时候会自动对代码进行分析,找到缺失的依赖下载并将信息写入到对应的go.mod文件中。
GO111MODULE
这个环境变量用来控制是否启用modules功能,还是按之前的套路来处理go的包管理。一般来说都用这个版本了,没什么理由不把这个打开(export GO111MODULE=on
)。如果你的代码是放在GOPATH之外的,那么默认就是打开的,放在GOPATH里的,默认是关闭的(我试下来是如此)。
go mod init
初始化代码模块,本质上就是创建一个go.mod文件。关于go.mod,可以看这里。
go get (-u)
获取或更新代码,同时会更新对应的go.mod文件。对modules来说,这个命令更多用在更新依赖的版本。
go mod graph
打印模块依赖图,也就是打印出模块之间的依赖关系。
go mod download
将依赖下载到本地cache。这个命令的应用场景一般是一个成熟项目,go.mod文件已经编辑好了,协同工作的成员可以在拉下代码之后直接用这个命令进行下载,然后就可以开始工作了。
go mod tidy
添加缺失的依赖,并移除没有使用的依赖。算是一个清理工具,一般在做版本的时候比较常用。
如果你在使用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
这个命令。
5. 模块定义 & 文件结构
代码根目录的约定
一般来说,一个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 创建一个包含两个包(如下列举)的模块:
- github.com/my/repo/foo
- github.com/my/repo/bar
那么你的go.mod文件的第一行将会定义一个模块,命名为:github.com/my/repo,而磁盘上的文件结构如下:
repo/
├── go.mod
├── bar
│ └── bar.go
└── foo
└── foo.go
6. 版本定义 & 更新
在之前我们已经说到,modules的出现并没有天翻地覆改变我们日常工作使用的命令和流程,但我们仍旧需要了解modules的很多细节,才能保证不犯错。这一节关于版本号相关的知识非常细节,也相当重要。
tag的命名规范
对于tag的命名,Go官方有明确的要求,不可以按自己的想法随便写。值得注意的点只有一点就是三位的版本号之前,必须添加一个v
,也就是说,所有的版本号都应该是:v1.2.3
这样的,而不是
1.2.3
这样。
看两个例子,打开链接之后观察下他们的版本号列表:
- Node.js的web服务器:expressjs/express
- Go的web服务器:gin-gonic/gin
版本选择
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.
- 在工具第一次处理同个依赖的多处引入的情况下,会引入该依赖的最高版本。
- 这个版本会在此后保持不变(go.mod中),即便这个依赖的新版本被release出来(作者更新)。这是为了保证产品100%可重复构建。用户可以自行选择升级与否。
值得注意的是原文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)
我没理解这是什么意思,后续需要尝试。猜测:
- 虽然go.mod文件中对于M的版本定义是v1.2.3,但允许的版本可以>=v1.2.3,并小于v2.x.x大版本
- 如果git clone一个带go.mod文件的库(clone下来的时候磁盘上无cache文件),使用go mod download,go.mod里对于这个库的定义是v1.2.3,但最新的release是v1.3.9,那么会下载v1.3.9?
版本引入
wiki原文可以查看:Semantic Import Versioning
使用过modules之后,会发现某些依赖的引入的最后带上了.vN
这样的字符,这里有点细节可以看下:
major版本是v2或更高的,在require的时候都需要在路径最后带上.vN
(官方给的例子里是/vN
,但我实际看下来几个包都是.vN
,先按实际例子来吧),比如说:
- require gopkg.in/go-playground/validator.v8 v8.18.2
- require gopkg.in/yaml.v2 v2.2.2
此外,在使用的时候(import):example.com/my/mod/mypkg
和example.com/my/mod.v2/mypkg
被认为是两个不同的包,会分开下载安装,且不会影响最后的编译。
如何更新依赖的版本
日常中对依赖进行版本升级或降级,应该使用go get
命令:
go get dep
将依赖升级到最新版本(等同于go get [email protected]
)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
7. 补充:go.mod & replace指令
官方wiki里关于go.mod的说明可以看这里:go.mod。
go.mod文件里的指令有:
- module, to define the module path;
- go, to set the expected language version;
- require, to require a particular module at a given version or later;
- exclude, to exclude a particular module version from use; and
- replace, to replace a module version with a different module version.
注意:exclude和replace指令只在主模块的go.mod文件中生效,在依赖中则会被忽略。
比如说依赖库里用到了 golang.org/x 里的包,你因为网络关系,在你自己的go.mod将这个库replace掉了,但下载完成之后你会发现在cache里还是会有golang.org/x的代码文件,因为第三方依赖可能用到它们了。
replace指令
在go.mod文件中,用得比较频繁的指令一般就只有require和replace。require不用多说,非常简单,replace还是有点细节的。
一般的使用场景:
- 使用本地代码:replace example.com/original/import/path => /your/forked/import/path
- 指定特定版本:replace example.com/some/dependency => example.com/some/dependency v1.2.3
- 指定特定下载地址(特别适合国内):replace golang.org/x/dependency => github.com/golang/dependency
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)
}
8. 实际经验
如果定义一个模块名为test-module
(一般不会这么干,都是以代码仓库为模块名,e.g github.com/gin-gonic/gin
)。
则:
- go.mod文件第一行为:
module test-module
- 该模块代码文件必须存放在
$GOPATH/src/test-module
下 - 代码包申明及引用相关:
- 代码文件的物理文件夹位置即包名
- 源码文件中的包名申明只需要路径的最后一段在代码中写明,不需要完整路径
- e.g
test-module/lib/dao
- 代码中的包为
package dao
- 其他代码引用的时候永远以模块名为起始,不能使用相对路径,即便是同模块的其他代码,e.g
import "test-module/lib/dao"
- 正因此,go项目的源码总是散在根目录下面的,不可以自行组织子文件夹
- ✗
$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