大家好,我是「云舒编程」,今天我们来聊聊GO依赖管理。
文章首发于微信公众号:云舒编程
关注公众号获取: 1、大厂项目分享 2、各种技术原理分享 3、部门内推
前言
Golang在项目早期只是单纯的使用GoPath进行依赖管理,但是GoPath无法管理同一个依赖的不同版本,并且由于把所有的依赖都放在同一个路径下,对于多项目的依赖管理非常不方便,于是增加了vendor,运行把依赖和项目放在一起,但是依旧没有解决版本问题,导致依赖关系不清楚,升级困难。在这段期间,也出现了很多第三方依赖管理工具,有点百家争鸣的意思。 直到Go 1.11官方才推出了依赖管理工具Go Module,才统一了六国,正式进入了"书同文 车同轨"的时代。
一、GoPath
GOPATH 是什么?
在Golang中存在两个重要目录,分别是GOROOT、GOPATH。
- GOROOT:Go 语言安装目录,属于 Go 语言顶级目录。
- GOPATH:用户工作空间目录,属于用户域范畴。
实际 Go 项目有一个或者多个 package 组成,package 按照来源可能分为:标准库、第三方库、项目私有库。标准库全部位于 GOROOT 目录中,而第三方库和私有库,都位于 GOPATH 目录。
GOPATH如何管理依赖?
当项目需要依赖package时,GoPath会优先在GOROOT中寻找同名包,如果在GOROOT找到了那么就会停止,否则就会继续在GOPATH中寻找。这样就会导致如果GOROOT、GOPATH存在同名包,那么GOROOT就会覆盖GOPATH中的包。
GOPATH缺点?
依赖包没有版本可言,都是指master最新代码。 如果不同项目想使用同一个包的不同版本,那么就无法实现。例如A项目想使用X包的v1版本,B项目想使用X包的v2版本,在GoPath中是无法实现的。
开始Golang这么设计是有原因的,因为Google是一个实践Mono Repo(把所有的相关项目都放在一个仓库中)的公司。但更多的公司和组织更多的是用Multi Repo(按模块分为多个仓库),GOPATH 至少解决了第三方源码依赖的问题,虽然它还不够完美。
二、vendor
上面有提到GoPath的问题是无法做到不同项目的依赖隔离,并且由于每次构建都有可能触发依赖包的更新,如果三方依赖包存在 bug 或不向下兼容,将直接影响 Golang 程序的稳定性。为了解决这些问题,于是在 Golang v1.5 版本中引入 vendor 机制。
所谓 vendor 机制,就是在不同的Golang项目的目录中,创建一个目录名为vendor的目录,将Golang项目的所有依赖包缓存到该目录中。
Golang 程序在编译时,Golang 编译器会优先在 vendor 目录中查找 Golang 程序依赖的三方包,而不是在 GOPATH 环境变量配置的本地路径下查找。
我们只需将 vendor 目录一起提交到代码仓库中,这样构建项目时就不会改变三方依赖包的版本。
但是随着项目不断迭代,依赖的三方包会越来越多,vendor目录会变得越来越大,将vendor目录提交到代码仓库,还会影响代码的下载速度。同时,vendor目录中的三方依赖包,也需要我们手动管理,比如手动记录依赖三方包的版本号,手动下载三方依赖包等。
三、Go Module
无论是GoPath还是vendor都没有解决同一个依赖包的不同版本问题,同时无法直观的梳理项目依赖,导致依赖的管理和升级、项目的构建都非常困难。 Go官方团队为了解决这个问题,在Go 1.11推出了Go Module打算彻底解决以上问题。同时实现了两个重要的目标:
- 准确记录项目依赖:记录项目依赖了哪些包,以及包的精确版本。同时可以导出全局依赖树,方便管理和升级。
- 可重复构建:在任何环境、平台的构建产物一致。
go.mod
类似java中的maven将依赖声明在.pom文件中一样,golang将依赖声明在go.mod中,下面是一个典型的go.mod文件组成:
go
module github.com/xx/test
go 1.18
require (
github.com/antlabs/strsim v0.0.3
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/fsnotify/fsnotify v1.6.0
github.com/gin-gonic/gin v1.8.1
github.com/gogf/gf v1.16.9
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
github.com/mozillazg/go-pinyin v0.20.0
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.9.3
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.690
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ocr v1.0.690
github.com/xuri/excelize/v2 v2.7.1
gorm.io/driver/mysql v1.5.1
gorm.io/gorm v1.25.2
)
exclude (
github.com/robfig/cron/v3 v3.0.1
)
replace (
gorm.io/gorm v1.25.2 => gorm.io/gorm v1.25.7
)
go.mod文件通常由以下几部分组成:
- module
- go版本声明
- require
- exclude
- replace
module
项目声明,一般采用仓库+module name的方式定义。如果我们项目提供了依赖包给其他人使用的话,对方可以根据这个module声明去对应的仓库中去查询,或者让go proxy到仓库中去查询。
go版本声明
它并不是指你当前使用的Go版本,而是指你的代码所需要的Go的最低版本。
require
声明了依赖包的路径和名字、版本 golang 对于依赖包的版本管理基于语义化,即版本号需要按照以下规定:
go
v<major>.<minor>.<patch>
major(主版本号): 当做了不兼容的API修改时,一般是重大架构、技术、功能升级,API已经不兼容原来的版本
minor(次版本): 当做了向下兼容的功能性新新增,一般是正常的版本、功能迭代,要求API向后兼容
patch(修订版本号):当做了向下兼容的问题修正,要求API向后兼容
假设我们需要引入依赖github.com/robfig/cron
,选择任何v1.x.y
都是兼容现在的代码的,例如v1.0.0
, v1.2.0
。 但是如果我们想使用v3.0.0
,直接去修改了go.mod
升级了依赖的版本到v3.0.0
,这个时候就会出现编译错误,因为主版本号升级后不承诺API的兼容性。
针对这个问题Go Module给的解决方案是,从主版本号的2
开始将主版本号加入到go moudle的path中,具体规则如下:
语义化版本 | module path | 导入go moudle中的包 |
---|---|---|
v1.x.y | github.com/robfig/cron | import "github.com/google/uuid" |
v2.x.y | github.com/robfig/cron/v2 | import "github.com/robfig/cron/v2" |
v3.x.y | github.com/robfig/cron/v3 | import "github.com/robfig/cron/v3" |
这样如果将项目依赖的外部go moudle的主版本号升级时,就需要切换moudle path和代码中导入package路径,同时对使用的不兼容的API做出修改调整。
exclude
如果你想在你的项目中排除某个依赖库的某个版本,你就可以使用这个字段。
go
exclude (
github.com/robfig/cron/v3 v3.0.1
)
replace
go
replace (
source latest => target latest
)
replace用来强制替换某些依赖库的版本。通常可以用于以下场景:
- 替换无法下载的包。比如有些包因为网络问题无法下载,如果这个包在 Github 上有镜像,那么可以替换为 Github上的包。
- 替换为fork仓库。比如有的包有 bug,在开源版本还没有修复时,可以暂时fork下来修复 bug,替换为 fork 版本,等修复后再使用开源包,这种是临时做法。
- 禁止被依赖。比如某个module不希望被直接引用,那么可以在 require 中把包的版本号都写为 v0.0.0,然后在下面 replace 中替换为实际的版本号。这样其他包引用这个包时,会因为找不到 v0.0.0而无法使用。
go.sum
go.sum 文件是什么
go.sum 文件中每行记录由包名、版本号、哈希值组成,使用空格分开。go.sum 文件存在的意义是,希望在任何环境中构建项目时使用的依赖包必须跟 go.sum 中记录的完全一致,从而达到一致构建的目的。
同时因为 go.mod 一般不会记录间接依赖,而 go.sum 会把直接依赖、间接依赖,全部记录上,所以go.sum 文件中行数会比 go.mod 文件函数多很多。
正常情况下,每个依赖包版本会包含两条记录:
- 第一条记录为该依赖包版本整体(所有文件)的哈希值,
- 第二条记录仅表示该依赖包版本中go.mod文件的哈希值
go
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
如果该依赖包版本没有go.mod文件,则只有第一条记录。如上面的例子中,v0.3.1表示该依赖包版本整体,而v0.3.1/go.mod表示该依赖包版本中go.mod文件。
依赖包版本中任何一个文件(包括go.mod)改动,都会改变其整体哈希值,此处再额外记录依赖包版本的go.mod文件主要用于计算依赖树时不必下载完整的依赖包版本,只根据go.mod即可计算依赖树。
go.sum 文件怎么用
当构建项目时,Go 会先从本地缓存中获取依赖包,然后计算本地依赖包的哈希值,和 go.sum 中的哈希值对比,如果不一致,就会拒绝构建。因为有可能本地缓存的包被篡改,也有可能时go.sum文件中的值被篡改,不过Go更倾向于相信 go.sum 文件中的哈希值,因为第一次写入的时候是经过校验的。
此时可以尝试删除 go.sum 文件,使用 go build 时会自动生成 go.sum 文件,重新写入哈希值,且第一次写入的时候,哈希值是经过校验和数据库校验的。这个校验和数据库的地址在环境变量 GOSUMDB 中有写到,它是一个提供依赖包哈希值查询的服务。
使用
go mod已经集成在go安装包中,只需要使用命令 go mod init [module name]即可初始化一个go mod。
推荐阅读
如果你也觉得我的分享有价值,记得点赞或者收藏哦!你的鼓励与支持,会让我更有动力写出更好的文章哦!
更多精彩内容,请关注公众号「云舒编程」