GO 依赖管理的变迁
GOPATH
GOPATH 是 go 语言中的一个环境变量, 其值相当于我们的工作目录。最早 go 是没有一套依赖管理机制的, 所有的依赖包都存储在 $GOPATH
下, 如果项目 A 和项目 B 依赖不同版本库 C「本地目录只会存储一个版本」, 如果 C 是不兼容的版本, 那么项目开发就会遇到问题。
go vender
go vendor 是 go 1.5 官方引入管理包依赖的方式。其做法是在当前开发的项目下创建一个 vender
目录, 存储当前项目的外部依赖。go 1.6 之后, 代码编译会优先从 vendor 目录寻找依赖包, 找不到再从 $GOPATH
中寻找。这种做法只是将不同项目的依赖做了隔离, 但其不区分包版本。go vender 会导致项目本身体积越来越大, 且没有记录依赖库的版本。
go moudles
go 在 1.11 之后引入的一种依赖管理解决方案,1.14 之后官方明确建议在生产环境上使用。有以下优势:
- 准确记录依赖版本
- 可重复构建
- 使用语义化版本控制规范
- 同一个版本的依赖库无需重复下载
go Modules
基本概念
modules:
go 在 1.11 之后引入的一种依赖管理解决方案,1.14 之后官方明确建议在生产环境上使用。
packages:
Go 程序是通过将包链接在一起进行构建程序。 package
由一个或多个源文件构成, 这些源文件共同声明属于该包的常量、类型、变量和函数, 并且可在同一包的所有文件中访问。 对于可导出的元素可以被另一个包使用。
module:
module
是存储在同一根目录下的包的集合, 且根目录包含 go.mod
文件。可以直接从版本控制存储库或代理服务器下载。
version:
版本号用于标识 module
的不可变快照, 以字母 v
开头, 符合语义化版本控制规范。
-
Major version: 主版本号, 对于向后不兼容的更新, 必须将 major version
增加, 同时将 minor version
, patch version
置为 0。例如, 对于暴露的接口新增一个必选参数, 或删除了一个 package
。
Minor version: 次版本号, 对于向后兼容的更新,必须将 minor version
增加, 同时将 patch version
置为 0。例如, 新增一个函数。
Patch version: 补丁版本号, 当 module
的改动对外暴露的接口没有发生变更, 必须将 patch version
增加。例如, 新增一个 bug
修复或优化。
pseudo-version:伪版本号, 当没有明确的版本号时, 会生成一个伪版本号。例如, 使用一个测试分支生成对应的伪版本号,由三部分组成:
使用 Modules
目前 Go modules 并不是默认开启,因此 Go 语言提供了 GO111MODULE
这个环境变量来作为 Go modules 的开关,其允许设置以下参数:
- auto:只要项目包含了 go.mod 文件的话启用 Go modules,目前在 Go1.11 至 Go1.14 中仍然是默认值。
- on:启用 Go modules,推荐设置。
- off:禁用 Go modules,不推荐设置。
设置 GO11MODULE 变量:
$ go env -w GO111MODULE=on
查看 GO11MODULE 的设置:
$ go env | grep GO111MODULE
GO111MODULE = "on"
创建 module
创建一个新的文件夹 cd $GOPATH/src
;mkdir -p zjl/learnmodule
, 新建一个 hello.go
文件
package hello
func Hello() string {
return "Hello, world."
}
使用 go mod init
命令将当前目录作为一个 module
$ go mod init example.com/zjl/hello
go: creating new go.mod: module example.com/hello
在当前目录下会生成 go.mod
文件, 对于子目录下的 package
, 导入路径由 module
路径加上子目录组成。例如: 在当前目录下新建一个子目录 word
, 这个 package 将自动成为 module「``example.com/zjl/hello``」
的一部分, 其导入路径为 example.com/hello``/word
添加依赖
在 hello.go
文件中引入一个 moudle
package hello
import "rsc.io/quote"
func Hello() string {
return quote.Hello()
}
使用 go mod tidy
命令, 添加当前编译当前项目所需的依赖, 移除不相关的依赖, 更新对应的 go.mod、go.sum
文件, 对应的 moudle 会缓存到本地的 $GOPATH/pkg/mod
目录下, mod
目录下, 每一个版本对应一个文件夹
$ go mod tidy
go: finding module for package rsc.io/quote
go: found rsc.io/quote in rsc.io/quote v1.5.2
$ cd $GOPATH/pkg/mod/rsc.io/ && ls
quote@v1.5.2 sampler@v1.3.0
查看当前项目的依赖,直接依赖 rsc.io/quote
的 v1.5.2
,间接依赖 golang.org/x/text
的 v0.0.0-20170915032832-14c0d48ead0c
,rsc.io/sampler
的 v1.3.0
,可以通过 go mod why
查看为什么会引入对应的间接依赖
$ cat go.mod
module example.com/hello
go 1.17
require rsc.io/quote v1.5.2
require (
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c // indirect
rsc.io/sampler v1.3.0 // indirect
)
$ go mod why rsc.io/sampler
#rsc.io/sampler
example.com/hello
rsc.io/quote
rsc.io/sampler
升级依赖
使用 go get
命令,升级依赖 moudle 的版本
命令 | 作用 |
---|---|
go get golang.org/x/text | 拉取依赖,会进行指定性拉取(更新),并不会更新所依赖的其它模块 |
go get -u golang.org/x/text | 更新现有的依赖,会强制更新它所依赖的其它全部模块,建议不使用 |
go get golang.org/x/text@latest | 拉取最新的版本 |
go get golang.org/x/text@master | 通过分支拉取 |
go get golang.org/x/text@v0.3.7 | 通过版本号拉取 |
go get golang.org/x/text@342b2e | 通过 commit hash 拉取 |
当前项目简介依赖 golang.org/x/text
的 v0.0.0-20170915032832-14c0d48ead0c
,查看对应的版本,并进行更新
$ go list -m -versions golang.org/x/text
golang.org/x/text v0.1.0 v0.2.0 v0.3.0 v0.3.1 v0.3.2 v0.3.3 v0.3.4 v0.3.5 v0.3.6 v0.3.7
$ go get golang.org/x/text@v0.3.7
在 go modules 中, 对于主版本的升级, 从 v2 起, 导入的 module 必须指定对应的主版本号。例如, 在项目中导入 rsc.io/sampler/v3
, 核心是为了解决版本冲突问题
$ go get rsc.io/quote/v3
// update hello.go
package hello
import (
"rsc.io/quote"
v3 "rsc.io/quote/v3"
)
func Hello() string {
v3.Concurrency()
return quote.Hello()
}
移除依赖
更新 hello.go
, 只使用 rsc.io/quote/v3
,但是对应的 go.mod 文件仍然依赖 rsc.io/quote v1.5.2
, 因为在构建包的时候, 可以很容易知道哪些需要添加, 但无法明确的知道哪些可以被安全的删除
$ cat hello.go
package hello
import (
v3 "rsc.io/quote/v3"
)
func Hello() string {
return v3.HelloV3()
}
$ cat go.mod
module example.com/hello
go 1.17
require (
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
)
require (
golang.org/x/text v0.3.7 // indirect
rsc.io/sampler v1.3.0 // indirect
)
使用 go mod tidy
命令, 添加当前编译当前项目所需的依赖, 移除不相关的依赖, 更新对应的 go.mod、go.sum
文件
$ go mod tidy
$ cat go.mod
module example.com/hello
go 1.17
require rsc.io/quote/v3 v3.1.0
require (
golang.org/x/text v0.3.7 // indirect
rsc.io/sampler v1.3.0 // indirect
)
版本选择算法
包依赖选择已经被证明是 NPC 问题,貌似通过布尔满足性论证的,具体证明过程「research.swtch.com/version-sat…
- 一个包有零个或多个版本依赖
- 安装这个包必须要安装其所有依赖
- 包的不同版本都可以有不同的依赖关系
- 不能同时安装一个包的多个版本
可以违背假设条件的方式来避免产生 NPC 问题。例如,允许安装一个包的不同主版本;依赖项不指定具体依赖的版本, 只指定依赖的最低版本。
go 版本选择算法主要基于以下几点出发:
- 尽可能使用开发者指定的依赖版本
- 构建过程应该可重现
- 算法简单, 能够快速的选择出对应依赖集合
- 版本升级由用户来控制
go 使用最小版本选择算法「MVS」来确定使用的 module 版本,通过此方法可以保证每次构建使用的版本都是确定的; 同一主版本号下,同一个包只依赖一个版本。当前算法存在的问题「前提是遵循办号兼容规则」
go.mod 文件
一个 module 在根路径上会定义一个 go.mod
文件, 用户描述当前项目的 module 信息. 文件中主要包含以下几个指令:
module
:
声明当前模块的路径,导入当前模块中的包时,均使用声明的路径作为前缀. 一个 go.mod
文件必须要包含一个 module
指令
module example.com/hello
go
:
用于声明当前模块使用哪个版本的 go 的语义进行开发. 对于模块中的包, 编译器不会使用比声明版本更高的特性; 如果使用旧版本的编译器编译高版本声明的 module, 将会报错.
go 1.17
require
:
require 指令声明当前项目所需依赖的版本. 对于一些依赖会自动添加 //indirect
, 用于声明当前包的间接依赖。
require golang.org/x/net v1.2.3
require (
golang.org/x/crypto v1.4.5 // indirect
golang.org/x/text v1.6.7
)
replace
:
将指定版本的依赖或任意版本的依赖进行替换,replace 只能作用于当前模块的构建. 可以替换为本地的模块或远程的模块
module A
replace (
golang.org/x/net v1.2.3 => example.com/fork/net v1.4.5
golang.org/x/net => example.com/fork/net v1.4.5
golang.org/x/net v1.2.3 => ./fork/net
golang.org/x/net => ./fork/net
)
module B // 构建时不会对 golang.org/x/net v1.2.3 进行 replace
require (
A v0.0.0
golang.org/x/net v1.2.3
)
使用 R 替换 C1.4, 构建时将使用 A1.2、B1.2、C1.4(R)、D1.3
exclude
:
构建时排除指定的版本, 只能作用于当前模块的构建
exclude golang.org/x/net v1.3
C1.3 被移除, 构建时将使用 C1.4
retract
:
用于声明相关的版本不应该被依赖,例如版本不再维护或废弃. 被废弃的版本将不再 go list -m -versions
命令中被展示,除非增加 -retracted
参数. 同时, 在使用 @latest
升级时, 标记为废弃的版本将会被忽略.
retract (
v1.0.0 // Published accidentally.
v1.0.1 // Contains retractions only.
)
go.sum 文件
在模块的根目录会生成对应的 go.sum 文件, 每行由模块名、版本、哈希组成:
bou.ke/monkey v1.0.2 h1:kWcnsrCNUatbxncxR/ThdYqbytgOIArtYWqcQLQzKLI=
bou.ke/monkey v1.0.2/go.mod h1:OqickVX3tNx6t33n1xvtTtu85YN5s6cKwVug+oHMaIA=
正常情况下,每个依赖包版本会包含两条记录,第一条记录为该依赖包版本整体(所有文件)的哈希值,第二条记录仅表示该依赖包版本中 go.mod
文件的哈希值,如果该依赖包版本没有 go.mod
文件,则只有第一条记录。
代码构建时,会从本地缓存$GOPATH/pkg/mod
中查找所有 go.mod
中记录的依赖包,并计算本地依赖包的哈希值,然后与 go.sum
中的记录进行对比,即检测本地缓存中使用的依赖包版本是否满足项目 go.sum
文件的期望。如果校验失败,说明本地缓存目录中依赖包版本
的哈希值和项目中 go.sum
中记录的哈希值不一致,go
命令将拒绝构建。主要还是用来检查依赖包是否被篡改, 保证可重复构建。
常用命令
go get moudle@lastest
添加依赖的最新版本, 不要使用 go get -u****
go get moudle@branch_name
添加特定分支作为依赖
go mod tidy
重新构建依赖, 移除不相关的依赖
go mod why module
查看为什么会有此依赖
go mod graph
查看依赖关系图
go list -m -versions module
查看对应的版本
go clean -modcache
删除模块缓存