文章目录
一.Golang依赖管理演进历程
Golang作为一门高效的服务端语言,和Java一样具备依赖管理体系,和Maven不同的事,Golang一直没有独立的版本管理平台,而是借助Github作为其版本管理/托管的主要平台。Golang的依赖管理发展历程如下:
go install/go get阶段(2010年至2012年)
在Golang诞生的初期,使用的依赖管理方式是通过go install及go get命令,先将程序所依赖的代码库下载到本地,并通过import引用这些库,来达到构建程序的目的。这会导致一个很严重的问题,因为无法指定版本,导致所有的Golang项目都只能依赖某个下载好的代码库。
godep阶段(2013.09)
godep提供了一个依赖文件,记录所有依赖具体的版本和路径,编译时将依赖下载到workspace中,然后切换到指定版本,并设置GOPATH访问(解决go get没有版本管理的缺陷)
gopkg.in阶段(2014.03)
通过import路径中添加版本号来标示不同版本,而实际代码存放于github中,go通过redirect获取代码。例如(import gopkg.in/yaml.v1,实际代码地址为:github.com/go-yaml/yam…)
vendor阶段(2015.06)
Go 1.5版本引入vendor(类似godep),存放于项目根目录,编译时优先使用vendor目录,之后再去GOPATH,GOROOT目录查找(解决GOPATH无法管控依赖变更和丢失的问题)
dep阶段(2016.08)
dep期望统一Go依赖管理,虽然提供了兼容其它依赖管理工具的功能,但是本质上还是利用GOPATH和vendor解决依赖管理
Go Modules阶段(2018.08)
Go 1.11发布的官方依赖管理解决方案,并最终统一了Go依赖管理。Go Modules以语义版本化和最小版本选择为核心,相比dep更具稳定性;同时也解决了vendor代码库依赖过于庞大,造成存储浪费的问题。Go Modules也是目前Golang程序开发中最常用的包依赖解决方案。
二.Go Modules的原理
关于Go Modules的安装以及使用这里不再赘述,可以参考网上的教程进行操作,本文主要想讨论的是Go Modules的包管理原理,以更好地理解其工作过程,解决工作中遇到的关于Go依赖相关的错误问题。
2.1.版本语义化
Go Modules提出了版本语义化规范,提倡通过Go Modules管理的包都应遵循这种版本命名规范:
主版本号(major).次版本号(minor).修订号(patch),如v1.2.3就是一个版本语义化的命名,要求如下:
- 主版本号:做了不兼容的 API修改
- 次版本号:向下兼容的功能性新增或改动
- 修订号:向下兼容的问题修正等
根据这种定义, 主版本号改动即视为不兼容之前大版本, 包管理机制也要使用不同主版本的包得到正确管理, 而次版本号以下的改动视为兼容, 会做合并。
2.2.依赖构建过程
最大版本选择
Go modules依赖构建的过程是依赖图深度优先遍历的过程,首先会以第一层直接依赖作为起始遍历节点,假设一个go modules依赖情况如下,那么会从A 1开始遍历,找到A 1的go.mod文件中定义的包名和版本号:B 1.2以及C 1.2,接着顺着B 1.2的go.mod找到D 1.3,借助C 1.2的go.mod找到D 1.4,然后找到E 1.2,就可以得到依赖关系:
将遍历得到的依赖关系组合成rough list(基本的依赖关系),可以看出D包同时有D 1.3和D 1.4两个版本的包被依赖了,根据版本语义化原则,v1.3和v1.4只是做了次版本号的更新,是可以向下兼容的,所以这里会取该包所依赖的“最新版本”D 1.4作为最终依赖关系(final list)。
依赖包升级过程
依赖包升级过程分为两种,一种是使用go get -u … 命令,这会导致整个项目的所有依赖都由上至下进行升级,全部依赖都会升级到最新的那个可获取的版本:
此时如果想让底层的包如E包仍然采用E1.2版本,那么就需要在go.mod中采用indirected的方式间接指定其版本或者直接replace这个包,让其只使用某个版本:
require (
E v1.2 // indirect
)
replace E => E v1.2
对于针对某特定依赖包的升级,如将C 1.2升级为C 1.3:go get C@1.3 ,那么按照最大版本选择原则,会选择依赖C 1.3的包并引入F 1.1和G 1.1这两个包,最终的final list为[A1,B1.2,C1.3,D1.4,E1.2,F1.1,G1.1],可以看出在更新指定包时,可能会引入新的依赖包。
依赖包降级过程
对某个依赖包执行降级操作,如将D 1.3降级为D 1.2:go get D@1.2 ,那么首先会将高于D 1.2版本的依赖包删除,即删除D 1.3和D 1.4,然后进行回溯,对依赖于D 1.3的B 1.2进行删除,并降级为B 1.1,同时对依赖于D 1.4的C 1.2删除,降级为C 1.1,并对D1.3和D1.4依赖的下游E1.2进行删除,所以此时得到的final list 为[A1,B1.1,C1.1,D1.2,E1.1]。所以对指定包执行降级操作会影响到该包的直接上下游,会把相关的依赖也进行降级和删除。
2.3.依赖包存储位置
Go modules所有的依赖都被存储在了$GOPATH/pkg/mod中,以包名@版本号的方式展示,很多时候包下载失败都是因为/pkg/mod文件夹没有开放权限导致依赖包无法下载。
2.4.go.sum校验原理
go.sum和go.mod文件是成对出现的,go.mod用于记录需要的依赖包+版本号,而go.sum文件为这些依赖包提供校验服务,来确保该依赖就是正确的依赖。
为什么要设计go.sum呢?因为go mod的实现是去中心化的,几乎所有的依赖都存储在github上,而go mod依赖的不过是github项目提交的commit id或者是tag id(也就是版本号),假如项目A提供版本v1.0.0的依赖包给项目B使用,那么项目A可以在项目B完全不知情的情况下,将项目A的v1.0.0版本删除并新增一个v1.0.0版本的依赖包,从版本号来看都是v1.0.0版本,但是依赖包可能发生了很大的改动,但项目B对项目A的版本改动完全不可知,这会导致依赖撰改问题。
所以对于项目A的每个版本,都需要经过go官方计算出该版本对应的校验和hash值来于该版本一一对应,如果发生了撰改,那么该hash值就会改变,从而会引发编译时的checksum mismatch错误,从而提醒依赖该包的项目注意。
go.sum中每个版本对应的hash值是通过sha256+base64算法计算生成,核心逻辑如下:
- 打开go.mod文件读取文件内容进行sha256哈希计算,得到sha256hash值
- 构建新的字符串 base64in = “sha256hash go.mod\n” ,中间用两个空格分隔
- 将base64in作为输入给base64进行编码得到base64encode
- 字符串拼接得到go.sum中一样的结果 h1:base64encode
通过上述逻辑就得到了go.sum中的文件内容:其中最下面两行的/go.mod h1:xxx为目前正在使用的依赖版本,而其余的为历史版本:
三.依赖编译常见问题
3.1.go mod tidy权限不足
- 一般给mod cache文件夹授权即可
sudo chmod -R 777 /Users/arong/go/pkg/mod/
3.2.checksum mismatch
- 这是由于校验依赖包的hash码失败,认为该包不可信导致编译失败问题,由代码合并或误修改引起
- 首先查询该包的hash码:访问https://sum.golang.org/lookup/{包名@版本号},如gopkg.in/yaml.v2 v2.2.8这个包被检查出mismatch,则访问https://sum.golang.org/lookup/gopkg.in/yaml.v2@v2.2.8,
就可以拿到正确的hash值:
728959
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
go.sum database tree
6438394
ISV58Q/1vfs4yXh7SRQ17crq8geYewPCoYuTD5C+2rM=
— sum.golang.org Az3grl1CA2g/LjiYCOI3yH9jeraei3QscJ2TiWNjFU0kmF7qkkp1C01JbBdF8V+KoZ+q2e2spxymlFVTUUKPWgIatA0=
- 手动将这两行替换掉go.sum中的两行即可
3.3.开发工具加载不出依赖
- 通常显示为开发工具中代码import爆红或者无法自动提示
- 首先打开Goland/Idea中的设置,点击Go-Go modules,允许Go modules扫描Index
- 项目中执行sudo go list -m -json all,扫描一遍依赖关系
3.4.莫名其妙的问题
使用go modules经常有莫名其妙的问题,可以把下述命令走一遍,可以解决八成问题:
- 删除go mod依赖:sudo go clean --modcache
- go mod 目录授权:sudo chmod -R 777 /Users/arong/go/pkg/mod/
- 下载依赖:sudo go mod tidy
- 扫描依赖:sudo go list -m -json all