「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。
Go 构建模式是怎么演化的?
Go 程序由 Go 包组合而成的,Go 程序的构建过程就是确定包版本、编译包以及将编译后得到的目标文件链接在一起的过程
Go 语言的构建模式历经了三个迭代和演化过程
- 最初期的 GOPATH
- 1.5 版本的 Vendor 机制
- 以及现在的 Go Module
GOPATH
Go 编译器可以在本地 GOPATH 环境变量配置的路径下,搜寻 Go 程序依赖的第三方包。如果存在,就使用这个本地包进行编译;如果不存在,就会报编译错误
缺点
- 在这种构建模式下,所有构建都离不开 GOPATH 环境变量
- 在这个模式下,Go 编译器并没有关注依赖包的版本,开发者也无法控制第三方依赖的版本,导致开发者无法实现可重现的构建
- 在未来可能会移除 GOPATH 构建模式,Go Module 构建模式将成为 Go 唯一的标准构建模式
Vendor 机制
- 开发者可以在项目目录下缓存项目的所有依赖,实现可重现构建
- 但 vendor 机制依旧不够完善,开发者还需要手工管理 vendor 下的依赖包,这就给开发者带来了不小的负担
Go Module
- 一个 Go Module 是一个 Go 包的集合
- module 是有版本的,所以 module 下的包也就有了版本属性
- 这个 module 与这些包会组成一个独立的版本单元,它们一起打版本、发布和分发
- 在 Go Module 模式下,通常一个代码仓库对应一个 Go Module
- 一个 Go Module 的顶层目录下会放置一个 go.mod 文件,每个 go.mod 文件会定义唯一一个 module,也就是说 Go Module 与 go.mod 是一一对应的
- Go 命令使用最小版本选择机制进行包依赖版本选择
main module
- go.mod 文件所在的顶层目录也被称为 module 的根目录
- module 根目录以及它子目录下的所有 Go 包均归属于这个 Go Module,这个 module 也被称为 main module
创建一个 Go Module
- 通过 go mod init 创建 go.mod 文件,将当前项目变为一个 Go Module;
- 通过 go mod tidy 命令自动更新当前 module 的依赖信息;
- 执行 go build,执行新 module 的构建
项目中创建一个 main.go
package main
import (
"github.com/valyala/fasthttp"
"go.uber.org/zap"
)
var logger *zap.Logger
func init() {
logger, _ = zap.NewProduction()
}
func fastHTTPHandler(ctx *fasthttp.RequestCtx) {
logger.Info("hello, go module", zap.ByteString("uri", ctx.RequestURI()))
}
func main() {
fasthttp.ListenAndServe(":8081", fastHTTPHandler)
}
这里依赖了两个第三方包,fasthttp、zap
go mod init
$ go mod init github.com/poloyy/hellomodule
go: creating new go.mod: module github.com/poloyy/hellomodule
go: to add module requirements and sums:
go mod tidy
- go mod init 在当前项目目录下创建了一个 go.mod 文件
- 这个 go.mod 文件将当前项目变为了一个 Go Module
- 项目根目录变成了 module 根目录
看看 go.mod 的文件内容
$ cat go.mod
module github.com/poloyy/hellomodule
go 1.17
- 第一行内容是用于声明 module 路径(module path)的
- 最后一行是一个 Go 版本指示符,用于表示这个 module 是在某个特定的 Go 版本的 module 语义的基础上编写的
go mod tidy
- 上面 go mod init 命令还输出了两行日志,提示可以使用 go mod tidy 命令,添加module 依赖以及校验和
- go mod tidy 命令会扫描 Go 源码,并自动找出项目依赖的外部 Go Module 以及版本,下载这些依赖并更新本地的 go.mod 文件
$ go mod tidy
go: finding module for package go.uber.org/zap
go: finding module for package github.com/valyala/fasthttp
go: found github.com/valyala/fasthttp in github.com/valyala/fasthttp v1.31.0
go: found go.uber.org/zap in go.uber.org/zap v1.19.1
- 由 go mod tidy 下载的依赖 module 会被放置在本地的 module 缓存路径下,默认值为$GOPATH/pkg/mod
- Go 1.15 及以后版本可以通过 GOMODCACHE 环境变量,自定义本地 module 的缓存路径
再次查看 go.mod 的文件内容
module github.com/poloyy/hellomodule
go 1.17
require (
github.com/valyala/fasthttp v1.31.0
go.uber.org/zap v1.19.1
)
具体的版本信息也会写到 go.mod 中
go.sum 文件
在 go.mod 下还有一个 go.sum 文件,内容大致如下
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
- 它存放了特定版本 module 内容的哈希值
- 这是 Go Module 的一个安全措施
- 当将来这里的某个 module 的特定版本被再次下载的时候,go 命令会使用 go.sum 文件中对应的哈希值,和新下载的内容的哈希值进行比对,只有哈希值比对一致才是合法的,这样可以确保你的项目所依赖的 module 内容,不会被恶意或意外篡改
- 因此,推荐把 go.mod 和 go.sum 两个文件与源码,一并提交到代码版本控制服务器上
再次执行构建
$ go build main.go
这样就能成功构建出可执行文件 main,然后运行它
$ ./main
{"level":"info","ts":1638496109.6682,"caller":"hello-go/01_bianyi.go:15","msg":"hello, go module","uri":"/foo/bar,"}
{"level":"info","ts":1638496113.153574,"caller":"hello-go/01_bianyi.go:15","msg":"hello, go module","uri":"/foo/ba"}
新开一个终端窗口,执行以下命令就能看到上面的日志了
curl localhost:8081/foo/bar
深入 Go Module 构建模式
Go 语言设计者在设计 Go Module 构建模式,来解决“包依赖管理”的问题时,进行了几项创新,这其中就包括语义导入版本 (Semantic Import Versioning),以及和其他主流语言不同的最小版本选择 (Minimal Version Selection) 等机制
Go Module 的语义导入版本机制
- go.mod 的 require 段中依赖的版本号,都符合
vX.Y.Z的格式 - 在 Go Module 构建模式下,一个符合 Go Module 要求的版本号,由前缀 v 和一个满足语义版本规范的版本号组成
按照语义版本规范
- 主版本号不同的两个版本是相互不兼容的
- 在主版本号相同的情况下,次版本号大都是向后兼容次版本号小的版本
- 补丁版本号也不影响兼容性
\
简单的🌰
以 logrus 为例,它有很多发布版本,从中选出两个版本 v1.7.0 和 v1.8.1 按照上面的语义版本规则,这两个版本的主版本号相同,新版本 v1.8.1 是兼容老版本 v1.7.0 的,所以都可以使用下面的包导入语句导入 logrus 包
import "github.com/sirupsen/logrus"
但假设要用 v2.0.0 版本时,主版本号和 1.7、1.8 明显不同了,它们互不兼容
如果想导入 logrus v2.0.0 版本,可以将将包主版本号引入到包导入路径中
import "github.com/sirupsen/logrus/v2"
Go 的语义导入版本机制,就是通过在包导入路径中加入主版本号的方式,来区别同一个包的不兼容版本。这样就可以同时导入一个包的两个不兼容版本了
import (
"github.com/sirupsen/logrus"
logv2 "github.com/sirupsen/logrus/v2"
)
Go Module 的最小版本选择原则
- 假设项目依赖 A、B 两个包,A、B 有一个共同的依赖包 C
- 但 A 依赖 C 的 v1.1.0 版本,而 B 依赖的是 C 的 v1.3.0 版本,并且此时 C 包的最新发布版为 C v1.7.0
问题来了:Go 命令是如何为 myproject 选出间接依赖包 C 的版本呢?选出的究竟是 v1.7.0、v1.1.0 还是 v1.3.0 呢
\
最新最大的版本 Latest Greatest
当前存在的主流编程语言,以及 Go Module 出现之前的很多 Go 包依赖管理工具都会选择依赖项的最新最大 (Latest Greatest) 版本;对应上面的就是 v1.7.0
Go Module 的最小版本
- Go Module 不光要考虑最新最大的稳定与安全,还要尊重各个 module 的述求:A 明明说只要求 C v1.1.0,B 明明说只要求 C v1.3.0;所以 Go 会在该项目依赖项的所有版本中,选出符合项目整体要求的【最小版本】
- 在上述 🌰 中,C v1.3.0 是符合项目整体要求的版本集合中的版本最小的那个,于是 Go 命令选择了 C v1.3.0,而不是最新最大的 C v1.7.0
Go 团队认为最小版本选择为Go 程序实现持久的和可重现的构建提供了最佳的方案
Go 各版本构建模式机制和切换
- 在 Go 1.11 版本中,Go 开发团队引入 Go Modules 构建模式
- 这个时候,GOPATH 构建模式与 Go Modules 构建模式各自独立工作,可以通过设置环境变量
GO111MODULE的值在两种构建模式间切换
- 随着 Go 语言的逐步演进,从 Go 1.11 到 Go 1.16 版本,不同的 Go 版本在
GO111MODULE为不同值的情况下,开启的构建模式都会变化,直到 Go 1.16 版本,GoModule 构建模式成为了默认模式
对比 GO111MODULE 不同版本下、不同构建模式下的行为特性
这里将 Go 1.13 版本之前、Go 1.13 版本、Go 1.16 版本
| GO111MODULE | < Go 1.13 | Go 1.13 | Go 1.16 |
|---|---|---|---|
| on | 任何路径下都开启 Go Module 构建模式 | 任何路径下都开启 Go Module 构建模式 | 默认值:任何路径下都开启 Go Module 构建模式 |
| auto | 默认值:使用 GOPATH 模式还是 Go Module 模式,取决于构建的源码目录所在位置,以及是否包含 go.mod 文件;如果构建的源码目录不在 $GOPATH/src 为根的目录体系下,且包含 go.mod 文件(两个缺一不可),那么就使用 Go Module 构建模式,否则使用 GOPATH 模式 | 默认值:只要当前目录或父目录下有 go.mod 文件时,就使用 Go Module 构建模式,无论源码目录是否在 $GOPATH 外面 | 只要当前目录或父目录下有 go.mod 文件时,就使用 Go Module 构建模式,无论源码目录是否在 $GOPATH 外面 |
| off | GOPATH 模式 | GOPATH 模式 | GOPATH 模式 |