对大多数 Go 开发者来说,Go 的依赖管理一直都很直接:所有的代码按模块划分,check-in 在互联网上几个广为人知的代码管理网站上,比如 http://github.com 或者 http://bitbucket.org;然后 Go 编译器就可以通过传统的 go get 或新的 go mod 来自动下载所有直接和间接依赖,而不用程序员自己动手。在 Golang 1.11 引入 go mod 命令之前,这个方法存在无法固定依赖树的缺陷而必须引入 vendor 目录的方法,而之后 go mod 则通过在代码 repo 里设置 semantic versioning tag 的方法完美地保持了版本一致性,非常方便。
但如果某些依赖不是存在于 Github 或者 Bitbucket,会怎么样?
实际上,有一类人群就深受这一麻烦困扰,那就是使用 visualstudio.com 的 Git repo 管理代码的程序员们。因为一些已知的问题,go 工具链无法兼容 visualstudio.com 的 Git 路径格式,因此如果 Go 开发者将自己的包代码保存于 visualstudio.com,则 go get 工具无法直接从 visualstudio.com 的 Git 代码仓库中下载依赖包,只能借助代理服务器进行中转。而即便引入代理服务器,我们也不一定高枕无忧:代理服务器要求转发用户名密码,这在对网络安全管理有一定要求的公司里又可能引起安全方面的担忧,所以也不能作为通用的解决方案。
在 Go 1.11 以前,程序员们必须依赖 GOPATH 管理自己的工作区,将各个包代码通过 go get 下载到 $GOPATH/src 目录下。因此我们就可以利用一种很简单的办法解决问题,即将包含 src/,bin/ 的Go工作区(即环境变量 GOPATH 指定的目录树)整个 check-in。每个模块代码作为 src/ 目录下的一个子目录。这种办法虽然违反了大多数 Go 开发者的习惯,但好歹比各个包分别 check-in 到不同 visualstudio.com Git repo 的办法容易管理;一定程度上还能统一同个项目下不同开发者的开发环境,所以也不失为一个好主意。
但是当 Go 1.11 以后,这个办法就不太管用了。因为新引入的 go mod 命令不再要求程序员显式定义 GOPATH。这个时候如果还继续在 Git repo 里维持完整的 GOPATH 结构,不但不必要,而且一定程度上还阻碍了我们使用新的 go mod。那么,有没有办法在 visualstudio.com 的 Git repo 中使用 go mod 的目录结构呢?
其实是有的。Go mod 引入了重命名包的方法解决这个问题。
假定我们有文件目录结构如下,myproject 是我们在 visualstudio.com 的顶层目录,clientlib/ 是实现辅助功能的包,而 cli/ 则实现了一个命令行可执行文件.。这个结构不服从标准的 GOPATH 模型,必须通过 go mod 模式管理。我们的需求是让 cli/main.go 引用 clientlib/ 包。
为了实现这一需求,我们需要在 myproject/cli/go.mod 里添加如下两行。第一行 require 命令定义了主命令行程序存在一个依赖,路径是一个URL, http://myproject.com/localref/clientlib;第二行 replace 命令告诉 Go 编译器,其实这个包的 URL 路径是瞎编的,它的真实路径是位于同一个 repo里上一层目录,即 ../clientlib。
顺便说一句,因为本地包没有明确的版本,我们必须总是将它的版本号命名为 latest,即目前代码库里最新的 check-in。当执行 go build 编译代码时,Go 编译器会自动编辑 go.mod,将 latest 替换为某个它自己计算出来的版本号,比如 v0.0.0-00010101000000-000000000000。这也是一个常用的小技巧,可以用来引用任何公网上却没有版本管理的 Go 包,比如 http://github.com/denisenkom/go-mssqldb,唯一的缺陷是某些编辑器插件(比如 vim-go)并不认识这个关键字,会在保存 go.mod 文件时触发错误,我们忽略就好。
注意:以下代码里我填的包版本号是 v0.0.0,这当然也是瞎编的:同在一个本地目录,显然无所谓版本。
将上述内容保存为 myproject/cli/go.mod 之后,就可以在 myproject/cli/main.go 里用我们瞎编的路径引用 clientlib 包了,代码如下。
这样,当我们在 myproject/cli/ 下执行 go build 命令时,Go 编译器就会到 ../clientlib 目录中寻找 clientlib 包定义。只要 myproject/clientlib/go.mod 正确定义了模块为 clientlib,就可以成功编译了。