之前写Go的时候都是直接Deb或者更暴力的Go get来安装依赖包,也是因为那时候为了赶项目直接走最直接的方法,现在有点时间了,来研究一下目前大家常使用的Go包管理器。
今天我们要介绍的两种方式是Go Mod和Go Vendor。
1.Go Mod
1.11版本之后开始能支持的一个包的管理,1.12版本正式GA解决的问题是golang不再依赖gopath的设置,下载下来的包可以直接使用。
1.1 存在问题:对使用者不友好
目前已经更新到1.12版本,但是go mod还是不太友好,使用起来不太方便。 开发的时候还是需要go get提前下载包,然后再应用。 只有要打包的时候执行一套命令,类似于下面这样:
go mod init ./
go build main.go 或 go build -mod=vendor main.go
go mod vendor #将包打到vendor文件夹下
一句话:用起来有点尴尬 。。。
https://zhuanlan.zhihu.com/p/59191567
go mod 引用本地其他项目的问题
build command-line-arguments: cannot load common/systemcall: malformed module path “common/systemcall”: missing dot in first path element
官方说是 import 前面必须要有.(例如 example.com ),因为自己把一些文件放到公共库项目了( common 模块,这个现在在 GOPATH 下面),多个其他项目都会有引用 common,这些项目都是放在自己的 git 服务器上( gitea 搭建的)。go.mod 里面说是可以定义 replace 包名,
module github.com/me/xxxxx
go 1.13
replace github.com/me/xxxxx => ./
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
)
https://blog.csdn.net/qq_43442524/article/details/105216529
相信大家在本地使用go mod开发项目时, 肯定会遇到很多依赖包的问题。因为go mod在最近的1.13中使用了Go GO111MODULE·
模块进行包管理。
设置Go GO111MODULE
有的小伙伴使用过程中肯定非常痛苦,因为不是很熟练, 而且里面还有很多不人性化的设置,网上的资料甚少,都是一篇文章你抄我,我抄你, 查阅大量文章之后无果,一怒之下我决定好好研究一下go mod。
遇到的问题
在本地下我有logCollect与logCollect/logBeegoWeb两个go modules模块, 那么我该如何在当前项目中加载另一个本地正在开发的模块呢?
问题解决
logBeegoWeb/go.mod
这里我go.mod里导入了很多包, 为了不影响观看体验, 所以这里就暂时没有写进去
module logBeegoWeb
go 1.14
require (
logCollect v0.0.0
)
replace logCollect => E:\Go\Gopath\src\demoCode\logCollect
1
2
3
4
5
6
7
8
名词解释
logCollect v0.0.0: logCollect是你要导入的包名称, 后面是版本号, 因为是本地开发,所以设置为v0.0.0
replace logCollect => E:\Go\Gopath\src\demoCode\logCollect 这里需要使用repalce将logCollect设置为不从网上拉取, 而是加入本地路径导入
想要导入本地项目, 只需要加入包名与版本号和replace 本地地址
注意: 该方法是导入其他mod下的包, 如果导入的包没有go mod init初始化
报错找不到mod文件: go: lock@v0.0.0: parsing ....\lock\go.mod: open E:\Go\Gopath\src\demoCode\lock\go.mod: The system cannot find the file specified.
在Go Modules查看, 导入本地项目成功!
如果想在你的模块如果想引入你本地其他地方的模块,可以尝试通过 replace 指定目录,而且前提是你的 article 也得 go 的一个模块,而不是按 gopath 下的某个包来引入。
我简答举个例子吧,比如现在有两个项目,分别是 blog 和 article,结果如下:
├─article
│ article.go
│ go.mod
│
├─blog
│ go.mod
│ main.go
blog 是应用的入口,main 所在位置,而 article 可以理解为你写的一个公共的库,其中提供了一个函数 Hello()。现在,要在 blog 中调用 article 中的 Hello() 函数。
article 模块中的 go.mod 内容如下:
module article
go 1.13
article.go 内容如下:
package article
func Hello() string {
return “Hello”
}
blog 模块中的 go.mod 内容如下:
go 1.13
require github.com/article v0.0.0-incompatible
replace github.com/article => ../article
此处的 replace 稍微介绍下,之所以要是 github.com/article 的格式,是因为在 go1.13 中, go module 名称规范要求路径的第一部分必须满足域名规范,否则可能汇报类似 malformed module path “article”: missing dot in first path element 这样的错误。当然,在 go1.12 不会有报这个错误。建议的话,如果是公司内部使用,可以替换成公司内部域名。
replace 的第二个参数指定了不从远程获取,而是本地某个路径下的模块替换 github.com/article。
main.go 的内容如下:
package main
import (
“fmt”
"github.com/article" )
func main() {
fmt.Println(“Hello”)
fmt.Println(article.Hello())
}
此时,在 blog 执行 go run main.go 是可以成功运行的。
https://zhuanlan.zhihu.com/p/98557072
https://www.cnblogs.com/wind-zhou/p/12824857.html
vendor概念最早是由Keith提出,用来存放依赖包。在版本1.5出现。例如gb项目提供了一个名为gsftp的示例项目,它有一个gsftp程序,在标准库之外有三个依赖项。golang.org/x/crypto/ssh, golang.org/x/crypto/ssh/agent和github.com/pkg/sftp
vendor的层级搜索
规则是:
从引用文件所在的vendor路径下面搜索,
如果没有找到,那么从上层目录的vendor路径下面搜索,
直到src的vendor路径下面搜索。
modules
Go 1.11版本支持临时环境变量GO111MODULE,通过该环境变量来控制依赖包的管理方式。当GO111MODULE的值为on时,那么就会使用modules功能,这种模式下,$GOPATH不再作为build时导入的角色,依赖包会存放在$GOPATH/pkg/mod目录下。工程中的依赖包也会从此目录下查找。有关该功能的介绍,可以看Go1.1.1新功能module的介绍及使用。
查找顺序
GO111MODULE=off时,如果一个包在vendor和$GOPATH下都存在,那么使用顺序为:
优先使用vendor目录下面的包,
如果vendor下面没有搜索到,再搜索$GOPATH/src下面的包,
如果$GOPATH下面没有搜索到,那么搜索$GOROOT/src下面的包,
要么完整使用vendor下面的包,要么完整使用$GOPATH下面的包,不会混合使用。
https://blog.csdn.net/benben_2015/article/details/91455497
go mod 生成 vendor
go mod可以使项目从GOPATH的强制依赖中独立出来,也就是说你的项目依赖不再需要放在在GOPATH下面了,每个工程的依赖包、版本可由当前工程独立管理!
GO111MODULE
GO111MODULE有三个值:off, on和auto(默认值)。
GO111MODULE=off,go命令行将不会支持module功能,寻找依赖包的方式将会沿用旧版本那种通过vendor目录或者GOPATH模式来查找。
GO111MODULE=on,go命令行会使用modules,而一点也不会去GOPATH目录下查找。
GO111MODULE=auto,默认值,go命令行将会根据当前目录来决定是否启用module功能。这种情况下可以分为两种情形:
当前目录在GOPATH/src之外且该目录包含go.mod文件
当前文件在包含go.mod文件的目录下面。
mod操作记录
1
2
3
export GO111MODULE=on
go mod init github.com/mutex73/ga
go.mod如何在项目中使用?
1.首先我们要在GOPATH/src 目录之外新建工程,或将老工程copy到GOPATH/src 目录之外。
PS:go.mod文件一旦创建后,它的内容将会被go toolchain全面掌控。go toolchain会在各类命令执行时,比如go get、go build、go mod等修改和维护go.mod文件。
go.mod 提供了module, require、replace和exclude四个命令
module语句指定包的名字(路径)
require语句指定的依赖项模块
replace语句可以替换依赖项模块
exclude语句可以忽略依赖项模块
1
2
3
go mod init + 模块名称 初始化模块
即go mod init hello
运行完之后,会在当前目录下生成一个go.mod文件,这是一个关键文件,之后的包的管理都是通过这个文件管理。
官方说明:除了go.mod之外,go命令还维护一个名为go.sum的文件,其中包含特定模块版本内容的预期加密哈希
go命令使用go.sum文件确保这些模块的未来下载检索与第一次下载相同的位,以确保项目所依赖的模块不会出现意外更改,无论是出于恶意、意外还是其他原因。 go.mod和go.sum都应检入版本控制。
go.sum 不需要手工维护,所以可以不用太关注。
注意:子目录里是不需要init的,所有的子目录里的依赖都会组织在根目录的go.mod文件里
export GO111MODULE=on
1、go mod init newapp
可以手动增加依赖go.uber.org/atomic v1.4.0或者让go自动发现和维护,下面build中会自动发现依赖包
2、go build main.go
vendor_test.go文件中增加了import “go.uber.org/zap”的语句,IDE提示报错,执行build后依赖包记录在go.mod中
1
3、go mod download
依赖包会自动下载到$GOPATH/pkg/mod,多个项目可以共享缓存的mod
4、go mod vendor
从mod中拷贝到项目的vendor目录下,这样IDE就可以识别了!
https://www.cnblogs.com/akidongzi/p/11772701.html
版本控制和语义化版本
包的版本控制总是一个包管理器绕不开的古老话题,自然对于我们的go modules也是这样。
我们将学习一种新的版本指定方式,然后深入地探讨一下golang官方推荐的semver即语义化版本。
控制包版本
在讨论go get进行包管理时我们曾经讨论过如何对包版本进行控制(文章在此),支持的格式如下:
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个痛点:
目标库需要打上符合要求的tag,如果tag不符合要求不排除日后出现兼容问题(目前来说只要正确指定tag就行,唯一的特殊情况在下一节介绍)
如果目标库没有打上tag,那么就必须毫无差错的编写大串的版本信息,大大加重了使用者的负担
基于以上原因,现在可以直接使用commit的hash来指定版本,如下:
go get github.com/mqu/go-notify@ef6f6f49
module my-module
require (
// other packages
github.com/mqu/go-notify ef6f6f49
)
随后我们运行go build或go mod tidy,这两条命令会整理并更新go.mod文件,更新后的文件会是这样:
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信息只有两个要求:
指定hash信息时不要在前面加上v,只需要给出commit hash即可
hash至少需要8位,与git等工具不同,少于8位会导致go mod无法找到包的对应版本,推荐与go mod保持一致给出12位长度的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
使用go1.11时设置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现在是给予耐心提出建议的阶段,评判还为时尚早。
replace的限制
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的适用范围受到了相当的限制:
可以使用本地包替换将生成代码纳入go modules的管理
对于直接import的顶层依赖,可以替换不能正常访问的包或是过时的包
go modules下import不再支持使用相对路径导入包,例如import “./mypkg”,所以需要考虑replace
除此之外的replace暂时没有什么用处,当然以后如果有变动的话说不定可以发挥比现在更大的作用。
发布go modules
本部分将讨论如何发布你的modules到github等开源仓库以供他人使用,放心这是相对来说最轻松的一部分。
go.sum不是锁文件
也许你知道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提供了安全性的校验)。
使用vendor目录
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
将源代码中使用了v2+版本包的import语句从import “my-module”改为import “my-module/v2”
仔细检查你的代码中所有my-module包的版本是否统一,修改那些不兼容的问题
在changelog中仔细列出所有breaking changes
当然,如果你觉得前面四步过于繁琐,注明你的用户需要指定+incompatible是一个暂时性的解决方案。
注意以上几点的话发布go modules也就是一个轻松的工作了。
小结
相比godep和vendor机制而言,go modules已经是向现代包管理器迈出的坚实一步,虽然还有不少僵硬甚至诡异的地方,但是个人还是推荐在go1.12发布后考虑逐步迁移到go modules,毕竟有官方的支持,相关issues的讨论也很活跃,不出意外应该是go包管理方案的最终答案,现在花上一些时间是值得的。
当然包管理是一个很大的话题,就算本文也只是讲解了其中的一二,以后我也许有时间会介绍更多go modules相关的内容。
总之go modules还是一个新兴事物,包管理器是一个需要不断在实践中完善的工具,如果你有建设性的想法请尽量向官方反馈。
https://blog.csdn.net/ytd7777/article/details/86898187