likes
comments
collection
share

[Golang早读] 谈谈 go.mod、go.sum、go.work

作者站长头像
站长
· 阅读数 13

Go Modules是Go 语言从 1.11 版本之后官方推出的版本管理工具。Modules官方定义为:

模块是相关Go包的集合。modules是源代码交换和版本控制的单元。 go命令直接支持使用modules,包括记录和解析对其他模块的依赖性。modules替换旧的基于GOPATH的方法来指定在给定构建中使用哪些源文件。

使用modules依赖管理的好处是非常明显的:

  • 自动下载依赖包
  • 不需要将代码再放入$GOPATH/src
  • 所有来自第三方的包都会指定版本(使用dep是无法指定第三方包的版本的)
  • 对于已经转移的包,可以用replace在go.mod文件中替换,不需要修改代码

如何使用Modules

  1. 把 golang 升级到 1.11+ (现在已经到1.19了)
  2. 设置 GO111MODULE

GO111MODULE

GO111MODULE 有三个值:off, onauto(默认值)

  • GO111MODULE=off,go命令行将不会支持module功能,寻找依赖包的方式将会沿用旧版本那种通过vendor目录或者GOPATH模式来查找。
  • GO111MODULE=on,go命令行会使用modules,而一点也不会去GOPATH目录下查找。
  • GO111MODULE=auto,默认值,go命令行将会根据当前目录来决定是否启用module功能。这种情况下可以分为两种情形:
    • 当前目录在$GOPATH/src之外且该目录包含go.mod文件
    • 当前文件在包含go.mod文件的目录下面。

当modules 功能启用时,依赖包的存放位置变更为$GOPATH/pkg,允许同一个package多个版本并存,且多个项目可以共享缓存的 module。

注:使用go modules最好搭配go proxy一起使用,否则在go get一些包时会出现get不到的问题

go.mod

go mod命令

命令说明
downloaddownload modules to local cache(下载依赖包到本地缓存)
editedit go.mod from tools or scripts (编辑go.mod)
graphprint module requirement graph (打印模块依赖图)
initinitialize new module in current directory (在当前目录初始化go.mod)
tidyadd missing and remove unused modules(拉取缺少的模块,移除不用的模块)
vendormake vendored copy of dependencies(将依赖复制到vendor下)
verifyverify dependencies have expected content (验证依赖是否正确)
whyexplain why packages or modules are needed(解释为什么需要依赖)

通常在项目中我们通过go get或者是go install的方式拉取指定版本的依赖包: 执行 go get命令,在下载依赖包的同时还可以指定依赖包的版本

  • 运行go get -u命令会将项目中的包升级到最新的次要版本或者修订版本
  • 运行go get -u=patch命令会将项目中的包升级到最新的修订版本
  • 运行go get [包名]@[版本号]命令会下载对应包的指定版本或者将对应包升级到指定的版本

当然我们也可以先不go get,在最后go run main.go时,go.mod同样会自动查找依赖自动下载

go.mod关键字

命令字作用
module指定包的名字(路径)
require指定依赖项模块
replace替换依赖项模块
exclude忽略依赖项模块

进入go.mod文件中,基本都是这样的结构

[Golang早读] 谈谈 go.mod、go.sum、go.work 注:indirect 表示这个库是间接引用进来的

go.sum

相较于go.mod,go.sum看上去就像是天书,可读性实在是太低,但是我们日常开发仍不得不要跟其打交道(通常是解决这个文件带来的合并冲突,或是想手动调整其中的内容)。

先下一个粗浅定义:go.sum就是依据hash来检测下载下来的依赖,是不是和该版本依赖复合

go.sum格式

go.sum的内容格式基本上是如图所示 [Golang早读] 谈谈 go.mod、go.sum、go.work

go.sum的每一行都是一个条目,大致是这样的格式

<module> <version>/go.mod <hash>

或者

<module> <version> <hash>
<module> <version>/go.mod <hash>

其中module是依赖的路径, version是依赖的版本号,hash是以h1开头的字符串,表示生成checksum的算法是第一版的hash算法(sha256)

需要特殊说明一下,version的确定规则,比较复杂:

一、项目是否打tag?

如果项目没有打 tag,会生成一个版本号,格式如下: v0.0.0-commit日期-commitID

比如 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=

引用一个项目的特定分支,比如 develop branch,也会生成类似的版本号: v当前版本+1-commit日期-commitID

比如 github.com/DATA-DOG/go-sqlmock v1.3.4-0.20191205000432-012d92843b00 h1:Cnt/xQ9MO4BiAjZrVpl0BiqqtTJjXUkWhIqwuOCVtWo=

二、项目有没有用 go module?

如果项目有用到 go module,那么就是正常地用 tag 来作为版本号。

比如 github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=

如果项目打了 tag,但是没有用到 go module,为了跟用了 go module 的项目相区别,需要加个 +incompatible 的标志。

比如 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=

三、项目用的 go module 版本是不是 v2+?

关于 go module v2+ 的特性,可以参考 Go 的官方文档:blog.golang.org/v2-go...。简单而言,就是通过让依赖路径带版本号后缀来区分同一个项目里不同版本的依赖,类似于 gopkg.in/xxx.v2 的效果。

对于使用了 v2+ go module 的项目,项目路径会有个版本号的后缀。

比如 github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=

Go在依赖管理时引入go.sum,是为了实现下面的目标

1. 提供分布式环境下的包管理依赖内容校验

Go爱用分布式的方式来管理包,这意味着缺乏一个可供信赖的中心来校验每个包的一致性。发布者在 GitHub 上给自己的项目打上 0.1 的 tag 之后,依旧可以删掉这个 tag ,提交不同的内容后再重新打个 0.1 的 tag。哪怕发布者都是老实人,发布平台也可能作恶。所以只能在每个项目里存储自己依赖到的所有组件的 checksum,才能保证每个依赖不会被篡改。

2. 作为 transparent log 来加强安全性

go.sum 还有一个很特别的地方,就是它不仅仅记录了当前依赖的checksum,还保留了历史上每次依赖的 checksum。这种做法效法了 transparent log 的概念。transparent log 旨在维护一个 Append Only 的日志记录,提高篡改者的作案成本,同时方便审查哪些记录是篡改进来的。根据 Proposal: Secure the Public Go Module Ecosystem 的说法,go.sum 之所以要用 transparent log 的形式记录历史上的每个checksum,是为了便于 sum db 的工作。

go.sum的引入带来的麻烦

1. 容易产生合并冲突

这恐怕是 go.sum 最为人诟病的地方了。由于许多项目都没有通过打tag的方式来管理发布,每个commit都相当于新发布一个版本,这导致拉取它们的代码时会偶尔往 go.sum 文件里插入一条新记录。go.sum会记录间接依赖的特性,更是让这种情况雪上加霜。

(go.sum这一块的内容非原创,来自谈谈go.sum - Go语言中文网 - Golang中文社区 (studygolang.com)

go.work

随着go 1.18版本的正式发布,多模块工作区也被引入(WorkSpaces),多模块工作区能够使开发者能够更容易地同时处理多个模块的工作, 如:方便进行依赖的代码调试(打断点、修改代码)、排查依赖代码 bug 。方便同时进行多个仓库/模块并行开发调试

go work支持命令

  • go work init 初始化工作区文件,用于生成go.work工作区文件

初始化并写入一个新的go.work到当前路径下,可以指定需要添加的代码模块

eg: go work init ./hello 将本地仓库hello添加到工作区

hello仓库必须是gomod依赖管理的仓库

  • go work use添加新的模块到工作区

eg:

go work use ./example 添加一个模块到工作区

go work use ./example ./example1 添加多个模块到工作区

go work use -r ./example 递归./example目录到当前工作区

删除命令: go work edit -dropuse=./example功能

  • go work sync将工作区的构建列表同步到工作区模块
  • go env GOWORK查看环境变量,查看当前工作区文件路径,可以排查工作区文件是否设置正确,go.work路径找不到可以使用GOWORK指定

go.work文件结构

go.work文件结构和go.mod文件结构类似,支持Go版本号、指定工作区和需要替代的仓库

[Golang早读] 谈谈 go.mod、go.sum、go.work

use指定使用的模块目录,可以使用go work use添加模块,也可以手动修改go.work工作区添加新的模块,在工作区中添加了模块路径,编译时会自动使用use中的本地代码进行编译

replaces替换依赖仓库地址,replaces命令与go.mod指令相同,用于替换项目中依赖的仓库地址,需要注意的是,replacesuse不能同时指定相同的本地代码路径

go.work文件优先级高于go.mod

在同时使用go.work和go.mod的replaces功能时,分别指定不同的代码仓库路径,go.work优先级高于go.mod中定义

//go.mod 中定义替换为本地仓库 example
replace ( 
    github.com/link1st/example => ./example 
)

//go.work 中定义替换为本地仓库 example1
replace ( 
    github.com/link1st/example => ./example1 
)

在代码构建时,使用的是go.work指定的example1仓库代码,go.work优先级别更高

一些注意点

  • 通常情况下,建议不要提交go.work到git上,因为他主要用于本地代码开发
  • 使用go.work时,不同的项目文件必须在有同一个根目录, 推荐在$GOPATH路径下执行,生成go.work文件
  • 目前仅go build会对go.work做出判断,go mod tidy不会影响工作区