深入Go Module之go.mod文件解析 - 鸟窝

文章推薦指數: 80 %
投票人數:10人

Go module就是一组统一打版和发布的package的集合,在根文件下有go.mod文件定义module path和依赖库的版本,package以子文件夹的形式存在module中,对 ... 2021年06月28日 Go bysmallnest 昨天在GopherChina2021大会上分享了《深入GoModule》,干货太多了,接下来的分几篇文章详细介绍下。

现在哪个新编程语言不都是提供库版本工具呢?Java、Python、nonjs、rust都有一套自己的库管理方法和库注册中心,不管它们的叫法如何。

Go最初推出来的时候并没有一个库管理方式,而是采用GOPATH的方式,所有的项目都必须放在GOPATH下,不止一次有从其它语言比如Java转过过来的同学问我:"为什么我在我的一个文件夹下建的项目没法编译呢?",那是因为他的文件夹没有建在GOPATH下,而等他想在GOPATH下建项目的时候,packagepath他又不知道该怎么写。

所以GO使用的GOPATH方式确实对新手来说是一件非常令人困惑的事。

而随着Go生态圈的快速壮大,另一个经典的库管理的问题也出现了。

就是我们项目中的依赖库经常出现APIbroken的情况,因为依赖的库相关接口改变了,导致我们的项目更新了依赖库后编译不过,我们不得不需要修改自己的代码以便适应依赖库的最新版本。

更困难的是,如果多个依赖库分别依赖第三个依赖库的第三个版本,版本冲突就出现了。

依赖库冲突几乎每个编程语言都有这样的问题,甚至操作系统也有DLL地狱问题,所以各种编程语言都尝试使用自己的方式解决依赖库版本的问题。

前面说了,Go最初是没有官方的库版本的方式的,都是靠第三方的工具实现,比如godep、glide、dep等,从2012年各种工具分别出现,大海淘沙,浮浮沉沉,最后也就有几个常用的工具大家在使用,dep是2017出现的一个版本,让人眼前一亮,而且也得到了Go官方的支持,项目也放在Golang组织之下golang/dep。

但是蜜月期没有多久,2018年RussCox经过深思熟虑以及一些早期的试验,决定go库版本的方式需要从头再来,深度集成go的各种工具(goget、golist等),实现精巧的最小化版本选择算法,解决brokenAPI共存等问题,所以dep就被废弃了,这件事还导致dep的作者相当的失望和数次争辩。

但是不管怎样,Go官方的库管理方式还是在2018年go1.11中实验性的推出了,通过设置一个环境变量GO111MODULE=on就可以弃用,并且期望go1.12正式退出,而环境变量GO111MODULE=on就可以去掉了。

可是没有想到的是,gomodule推出后问题多多,现在每一个go的版本中都有对gomodule修改,导致这个特性一直没有最终完成,这也是我吐槽它的地方:都快三年了,一个feature都开发那么久,而且未来的go1.17、go1.18还有一些改变,同学们,还学的动吗? go官方库管理方式叫做gomodule。

先前,我们的库都是以package来组织的,package以一个文件或者多个文件实现单一的功能,一个项目包含一个package或者多个package。

Gomodule就是一组统一打版和发布的package的集合,在根文件下有go.mod文件定义modulepath和依赖库的版本,package以子文件夹的形式存在module中,对packagepath就是modulepath+"/"+packagename的形式。

一般我们项目都是单module的形式,项目主文件夹下包含go.mod,子文件夹定义package,或者主文件夹也是一个package。

但是一个项目也可以包含多个module,只不过这种方式不常用而已。

go.mod gomodule最重要的是go.mod文件的定义,它用来标记一个module和它的依赖库以及依赖库的版本。

会放在module的主文件夹下,一般以go.mod命名。

一个go.mod内容类似下面的格式: 123456789101112131415161718192021222324252627modulegithub.com/panicthis/modfilego1.16require( github.com/cenk/backoffv2.2.1+incompatible github.com/coreos/bboltv1.3.3 github.com/edwingeng/doublejumpv0.0.0-20200330080233-e4ea8bd1cbed github.com/stretchr/objxv0.3.0//indirect github.com/stretchr/testifyv1.7.0 go.etcd.io/bboltv1.3.6//indirect go.etcd.io/etcd/client/v2v2.305.0-rc.1 go.etcd.io/etcd/client/v3v3.5.0-rc.1 golang.org/x/netv0.0.0-20210610132358-84b48f89b13b//indirect golang.org/x/sysv0.0.0-20210611083646-a4fc73990273//indirect)exclude( go.etcd.io/etcd/client/v2v2.305.0-rc.0 go.etcd.io/etcd/client/v3v3.5.0-rc.0)retract(v1.0.0//废弃的版本,请使用v1.1.0) 虽然是一个简单的文件,但是里面的乾坤不少,让我们依次介绍它们。

语义化版本2.0.0 Gomodule遵循语义化版本规范2.0.0。

语义化版本规范2.0.0规定了版本号的格式,每个字段的意义以及版本号比较的规则等等。

如果你想为你的项目发版,你可以设置tag为上面的格式,比如v1.3.0、v2.0.0-rc.1等等。

metadata中在Go版本比较时是不参与运算的,只是一个辅助信息。

modulepath go.mod的第一行是modulepath,一般采用仓库+modulename的方式定义。

这样我们获取一个module的时候,就可以到它的仓库中去查询,或者让goproxy到仓库中去查询。

1modulegithub.com/panicthis/modfile 如果你的版本已经大于等于2.0.0,按照Go的规范,你应该加上major的后缀,modulepath改成下面的方式: 1modulegithub.com/panicthis/modfile/v2 1modulegithub.com/panicthis/modfile/v3 而且引用代码的时候,也要加上v2、v3、vx后缀,以便和其它major版本进行区分。

这是一个很奇怪的约定,带来的好处是你一个项目中可以使用依赖库的不同的major版本,它们可以共存。

godirective 第二行是godirective。

格式是go1.xx,它并不是指你当前使用的Go版本,而是指名你的代码所需要的Go的最低版本。

1go1.16 因为Go的标准库也有所变化,一些新的API也被增加进来,如果你的代码用到了这些新的API,你可能需要指名它依赖的go版本。

这一行不是必须的,你可以不写。

require require段中列出了项目所需要的各个依赖库以及它们的版本,除了正规的v1.3.0这样的版本外,还有一些奇奇怪怪的版本和注释,那么它们又是什么意思呢? 正式的版本号我们就不需要介绍了,大家都懂: 1github.com/coreos/bboltv1.3.3 伪版本号 1github.com/edwingeng/doublejumpv0.0.0-20200330080233-e4ea8bd1cbed 上面这个库中的版本号就是一个伪版本号v0.0.0-20200330080233-e4ea8bd1cbed,这是gomodule为它生成的一个类似符合语义化版本2.0.0版本,实际这个库并没有发布这个版本。

正式因为这个依赖库没有发布版本,而gomodule需要指定这个库的一个确定的版本,所以才创建的这样一个伪版本号。

gomodule的目的就是在go.mod中标记出这个项目所有的依赖以及它们确定的某个版本。

这里的20200330080233是这次提交的时间,格式是yyyyMMddhhmmss,而e4ea8bd1cbed就是这个版本的commitid,通过这个字段,就可以确定这个库的特定的版本。

而前面的v0.0.0可能有多种生成方式,主要看你这个commit的baseversion: vX.0.0-yyyymmddhhmmss-abcdefabcdef:如果没有baseversion,那么就是vX.0.0的形式 vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef:如果baseversion是一个预发布的版本,比如vX.Y.Z-pre,那么它就用vX.Y.Z-pre.0的形式 vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdefabcdef:如果baseversion是一个正式发布的版本,那么它就patch号加1,如vX.Y.(Z+1)-0 indirect注释 123go.etcd.io/bboltv1.3.6//indirectgolang.org/x/netv0.0.0-20210610132358-84b48f89b13b//indirectgolang.org/x/sysv0.0.0-20210611083646-a4fc73990273//indirect 有些库后面加了indirect后缀,这又是什么意思的。

如果用一句话总结,间接的使用了这个库,但是又没有被列到某个go.mod中,当然这句话也不算太准确,更精确的说法是下面的情况之一就会对这个库加indirect后缀: 当前项目依赖A,但是A的go.mod遗漏了B,那么就会在当前项目的go.mod中补充B,加indirect注释 当前项目依赖A,但是A没有go.mod,同样就会在当前项目的go.mod中补充B,加indirect注释 当前项目依赖A,A又依赖B,当对A降级的时候,降级的A不再依赖B,这个时候B就标记indirect注释 incompatible 有些库后面加了incompatible后缀,但是你如果看这些项目,它们只是发布了v2.2.1的tag,并没有+incompatible后缀。

1github.com/cenk/backoffv2.2.1+incompatible 这些库采用了go.mod的管理,但是不幸的是,虽然这些库的版major版本已经大于等于2了,但是他们的modulepath中依然没有添加v2、v3这样的后缀。

所以ghomodule把它们标记为incompatible的,虽然可以引用,但是实际它们是不符合规范的。

exclude 如果你想在你的项目中跳过某个依赖库的某个版本,你就可以使用这个段。

1234exclude( go.etcd.io/etcd/client/v2v2.305.0-rc.0 go.etcd.io/etcd/client/v3v3.5.0-rc.0) 这样,Go在版本选择的时候,就会主动跳过这些版本,比如你使用goget-u......或者gogetgithub.com/xxx/xxx@latest等命令时,会执行versionquery的动作,这些版本不在考虑的范围之内。

replace replace也是常用的一个手段,用来解决一些错误的依赖库的引用或者调试依赖库。

123replacegithub.com/coreos/bbolt=>go.etcd.io/bboltv1.3.3replacegithub.com/panicthis/Av1.1.0=>github.com/panicthis/Rv1.8.0replacegithub.com/coreos/bbolt=>../R 比如etcdv3.3.x的版本中错误的使用了github.com/coreos/bbolt作为bbolt的modulepath,其实这个库在它自己的go.mod中声明的modulepath是go.etcd.io/bbolt,又比如etcd使用的grpc版本有问题,你也可以通过replace替换成所需的grpc版本。

甚至你觉得某个依赖库有问题,自己fork到本地做修改,想调试一下,你也可以替换成本地的文件夹。

replace可以替换某个库的所有版本到另一个库的特定版本,也可以替换某个库的特定版本到另一个库的特定版本。

retract retract是go1.16中新增加的内容,借用学术界期刊撤稿的术语,宣布撤回库的某个版本。

如果你误发布了某个版本,或者事后发现某个版本不成熟,那么你可以推一个新的版本,在新的版本中,声明前面的某个版本被撤回,提示大家都不要用了。

撤回的版本tag依然还存在,goproxy也存在这个版本,所以你如果强制使用,还是可以使用的,否则这些版本就会被跳过。

和exclude的区别是retract是这个库的owner定义的,而exclude是库的使用者在自己的go.mod中定义的。

Newer 深入GoModule之讨厌的v2 Older wioterminal掌机开发板试用报告 原创图书 分类 Android12C++1DOTNET1Docker5Go178Java64Linux7Rust12Scala18分享1前端开发18区块链8大数据60工具28数据库3架构26算法4管理2网络编程13读书笔记2运维2高并发编程20 标签云 AndroidApacheBenchBowerC#CDNCQRSCRCCSSCompletableFutureComsatCuratorDSLDisruptorDockerEmberFastJsonFiberGAEGCGnuplotGoGradleGruntGulpHadoopHazelcastIPFSIgniteJVMJavaKafkaLambdaLinuxLongAdderMathJaxMavenMemcachedMetricsMongoNetty 归档 November20211October20213August20212July20213June20214May20213April20212March20212February20211January20212December20203November20202September20201August20201July20202June20202May20204April20201March20203February20202January20205December20196November20192October20196September20197August20197July20197June20191May20192April20193March20191February20196January20195December20182November20184October20182September20186August20185July20183June20183May20182April20181March20186February20184January20183December20177November20174October20176September20174August20174July20174June20177May20174April20177March20176February20173January20173December20165November20167October20166September20165August20164July201612June201614May20166April201614March20167February20168January20161December20153November201510October20159September201512August201512July201512June20158May20157April201515March201510February20154January201512December201428November201412October201410September201428August201419July20141 近期文章 Go并发编程一年回顾(2021) Go泛型系列:再简化,省略接口 [译]更快的时间解析 强制更改Go标准库的实现 Go泛型是怎么实现的? 友情链接 技术栈   开发者头条 码农周刊 编程狂人周刊 importnew 并发编程网   github stackoverflow javacodegeeks infoq dzone leetcode jenkov HowToDoInJava javadesignpatterns   Netflix技术博客 TechieDelight Linkedin技术博客 Dropbox技术博客 Facebook技术博客 淘宝中间件团队 美团技术博客 360技术博客 小米信息部技术团队  首页  归档  github  网站群    Go汇编示例    GoWeb开发示例    Go数据库开发教程    RPCX官网    RPC开发指南  Scala集合技术手册  关于



請為這篇文章評分?