还有半个月go1.12就要发布了。这是首个将go modules归入正式支持的稳定版本。html
距离go modules随着go1.11正式面向广大开发者进行体验也已通过去了半年,这段时间go modules也发生了一些变化,借此机会我想再次深刻探讨go modules的使用,同时对这个新生包管理方案作一些思考。git
本文索引
包的版本控制老是一个包管理器绕不开的古老话题,天然对于咱们的go modules也是这样。github
咱们将学习一种新的版本指定方式,而后深刻地探讨一下golang官方推荐的semver
即语义化版本。golang
在讨论go get进行包管理时咱们曾经讨论过如何对包版本进行控制(文章在此),支持的格式以下:chrome
vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef vX.0.0-yyyymmddhhmmss-abcdefabcdef vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdefabcdef vX.Y.Z
在go.mod文件中咱们也须要这样指定,不然go mod没法正常工做,这带来了2个痛点:npm
基于以上缘由,如今能够直接使用commit的hash来指定版本,以下:json
# 使用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文件,更新后的文件会是这样:api
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信息只有两个要求:安全
v
,只须要给出commit hash便可然而这和咱们理想中的版本控制方式彷佛仍是有些出入,是否是以为。。。有点不直观?接下来介绍的语义化版本也许能带来一些改观。
golang官方推荐的最佳实践叫作semver
,这是一个简称,写全了就是Semantic Versioning
,也就是语义化版本。
通俗地说,就是一种清晰可读的,明确反应版本信息的版本格式,更具体的规范在这里。
如规范所言,形如vX.Y.Z
的形式显然比一串hash更直观,因此golang的开发者才会把目光集中于此。
semver
简化版本指定的做用是显而易见的,然而仅此一条理由显然有点缺少说服力,毕竟改进后的版本指定其实也不是那么麻烦,对吧?
那么为什么要引入一套新的规范呢?
我想这可能与golang一向重视工程化的哲学有关:
不要删除导出的名称,鼓励标记的复合文字等等。若是须要不一样的功能,添加 新名称而不是更改旧名称。若是须要完整中断,请建立一个带有新导入路径的新包。 -go modules wiki
经过semver
对版本进行严格的约束,能够最大程度地保证向后兼容以及避免“breaking changes”,而这些都是golang所追求的。二者一拍即合,因此go modules提供了语义化版本的支持。
若是你使用和发布的包没有版本tag或者处于1.x版本,那么你可能体会不到什么区别,由于go mod所支持的格式从始至终是遵循semver
的,主要的区别体如今v2.0.0
以及更高版本的包上。
“若是旧软件包和新软件包具备相同的导入路径,则新软件包必须向后兼容旧软件包。” - go modules wiki
正如这句话所说,相同名字的对象应该向后兼容,然而按照语义化版本的约定,当出现v2.0.0
的时候必定表示发生了重大变化,极可能没法保证向后兼容,这时候应该如何处理呢?
答案很简单,咱们为包的导入路径的末尾附加版本信息便可,例如:
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认为不一样的导入路径意味着不一样的包。
不过这里有几个例外能够不用参照这种写法:
gopkg.in
格式时可使用等价的require gopkg.in/some/pkg.v2 v2.0.0
+incompatible
就能够不须要指定/vN
,例如:require some/pkg v2.0.0+incompatible
GO111MODULE=off
将取消这种限制,固然go1.12里就不能这么干了除此之外的状况若是直接使用v2+版本将会致使go mod报错。
v2+版本的包容许和其余不一样大版本的包同时存在(前提是添加了/vN
),它们将被当作不一样的包来处理。
另外/vN
并不会影响你的仓库,不须要建立一个v2对应的仓库,这只是go modules添加的一种附加信息而已。
固然若是你不想遵循这一规范或者须要兼容现有代码,那么指定+incompatible
会是一个合理的选择。不过如其字面意思,go modules不推荐这种行为。
眼尖的读者可能已经发现了,semver
很眼熟。
是的,REST api
是它的最忠实用户,像xxx.com/api/v2/xxx
的最佳实践咱们恐怕都司空见惯了,因此golang才会要求v2+的包使用pkg/v2
的形式。然而把REST api
的最佳实践融合进包管理器设计,真的会是又一个最佳实践吗?
我以为未必如此,一个显而易见的缺点就在于向后兼容上,主流的包管理器都只采用semver
的子集,最大的缘由在于若是只提供对版本的控制,而把前后兼容的责任交由开发者/用户相对于强行将无关的信息附加在包名上来讲可能会形成必定的迷惑,可是这种作法能够最大限度的兼容现有代码,而golang则须要修改mod文件,修改引入路径,分散的修改每每致使潜在的缺陷,考虑到现有的golang生态这一作法显得不那么明智。同时将版本信息绑定进包名对于习惯了传统包管理器方案的用户(npm,pip)来讲显得有些怪异,可能须要花上一些额外时间适应。
不过检验真理的标准永远都是实践,随着go1.12的发布咱们最终会见分晓,对于go modules如今是给予耐心提出建议的阶段,评判还为时尚早。
go mod edit -replace
无疑是一个十分强大的命令,但强大的同时它的限制也很是多。
本部分你将看到两个例子,它们分别阐述了本地包替换的方法以及顶层依赖与间接依赖的区别,如今让咱们进入第一个例子。
replace除了能够将远程的包进行替换外,还能够将本地存在的modules替换成任意指定的名字。
假设咱们有以下的项目:
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:
package main import "my/example/pkg" func main() { pkg.Hello() }
咱们的pkg.go相对来讲很简单:
package pkg import "fmt" func Hello() { fmt.Println("Hello") }
重点在于go.mod文件,虽然不推荐直接编辑mod文件,但在这个例子中与使用go mod edit
的效果几乎没有区别,因此你能够尝试本身动手修改my-mod/go.mod:
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(wiki说本地替换不用指定,然而我试了报错),因此在my-mod/go.mod中咱们须要先指定依赖的包,即便它并不存在。对于一个会被replace的包,若是是用本地的module进行替换,那么能够指定版本为v0.0.0
(对于没有使用版本控制的包只能指定这个版本),不然应该和替换包的指定版本一致。
再看replace my/example/pkg => ./pkg
这句,与替换远程包时同样,只是将替换用的包名改成了本地module所在的绝对或相对路径。
一切准备就绪,咱们运行go build
,而后项目目录会变成这样:
tree my-mod my-mod ├── go.mod ├── main.go ├── my-mod └── pkg ├── go.mod └── pkg.go
那个叫my-mod的文件就是编译好的程序,咱们运行它:
./my-mod Hello
运行成功,my/example/pkg
已经替换成了本地的pkg
。
同时咱们注意到,使用本地包进行替换时并不会生成go.sum所需的信息,因此go.sum文件也没有生成。
本地替换的价值在于它提供了一种使自动生成的代码进入go modules系统的途径,毕竟无论是go tools仍是rpc工具,这些自动生成代码也是项目的一部分,若是不能归入包管理器的管理范围想必会带来很大的麻烦。
若是你由于golang.org/x/...
没法获取而使用replace进行替换,那么你确定遇到过问题。明明已经replace的包为什么还会去未替换的地址进行搜索和下载?
解释这个问题前先看一个go.mod的例子,这个项目使用的第三方模块使用了golang.org/x/...
的包,但项目中没有直接引用它们:
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 )
注意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/net
被github.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
注释。
replace惟一的限制是它只能处理顶层依赖。
这样限制的缘由也很好理解,由于对于包进行替换后,一般不能保证兼容性,对于一些使用了这个包的第三方module来讲可能意味着潜在的缺陷,而容许顶层依赖的替换则意味着你对本身的项目有充足的自信不会由于replace引入问题,是可控的。至关符合golang的工程性原则。
也正如此replace的适用范围受到了至关的限制:
import "./mypkg"
,因此须要考虑replace除此以外的replace暂时没有什么用处,固然之后若是有变更的话说不定能够发挥比如今更大的做用。
发布go modules
也许你知道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提供了安全性的校验)。
golang一直提供了工具选择上的自由性,若是你不喜欢go mod的缓存方式,你可使用go mod vendor
回到godep
或govendor
使用的vendor
目录进行包管理的方式。
固然这个命令并不能让你从godep之类的工具迁移到go modules,它只是单纯地把go.sum中的全部依赖下载到vendor目录里,若是你用它迁移godep你会发现vendor目录里的包回合godep指定的产生至关大的差别,因此请务必不要这样作。
咱们举第一部分中用到的项目作例子,使用go mod vendor
以后项目结构是这样的:
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机制:
go build -mod=vendor ./my-module a notify!
构建成功。当发布时也只须要和使用godep时同样将vendor目录带上便可。
其实这是第一部分的老生常谈,当你发布一个v2+版本的库时,须要进行如下操做:
module my-module
改为module my-module/v2
import "my-module"
改成import "my-module/v2"
my-module
包的版本是否统一,修改那些不兼容的问题+incompatible
是一个暂时性的解决方案。注意以上几点的话发布go modules也就是一个轻松的工做了。
相比godep和vendor机制而言,go modules已是向现代包管理器迈出的坚实一步,虽然还有很多僵硬甚至诡异的地方,可是我的仍是推荐在go1.12发布后考虑逐步迁移到go modules,毕竟有官方的支持,相关issues的讨论也很活跃,不出意外应该是go包管理方案的最终答案,如今花上一些时间是值得的。
固然包管理是一个很大的话题,就算本文也只是讲解了其中的一二,之后我也许有时间会介绍更多go modules相关的内容。
总之go modules仍是一个新兴事物,包管理器是一个须要不断在实践中完善的工具,若是你有建设性的想法请尽可能向官方反馈。
go modules的官方wiki也上线一段时间了,这篇文件基本上是与其结合的查漏补缺,同时也夹杂了一些我的看法,因此不免有所错误疏漏,欢迎指正。