共计 5207 个字符,预计需要花费 14 分钟才能阅读完成。
学习 go mod 的心得
@Spike
参考
- 官网 Guide
- Russ Cox 的
Go & Versioning
11 篇
请留神:这篇文章次要是学习 go mod 的应用和对 Go & Versioning
这个系列文章的学习心得。
如果你看完且了解了这个系列的文章,那么接下来的内容应该对你没有任何帮忙。
蕴含内容
- 什么包治理,有哪些内容?
- Go 的包治理是怎么的。
- 怎么在 Go 外面实现最好的实际。
大部分新一点的语言都有本人的包管理系统,例如 CPAN(perl
), pip(Python
), gem(Ruby
)。
一般来说,大部分语言的包管理系统次要都集中在拉取包和依赖。然而本文介绍的 go mod
还有更加丰盛的性能和设计指标。这些个性和下面说的那些包管理系统或多或少有一些不同,至多能够叫做「版本化的、有依赖的模块治理」。
Go 版本治理的次要指标
- 晋升应用内部
module
的体验。 - 更加准确的治理咱们的代码和模块。
- 稳固的构建 reproducible builds。
- 标记应用的其余模块(甚至能够记录在最终的二进制中)。
背景:模块和包 module
and package
-
模块 (
module
) 是一堆有雷同前缀的包 (package
) 的汇合。module
有版本号(并且是有逻辑上的意义的)。package
没有版本号。go mod
是针对的module
。
-
模块的命名,一般来说是
organization/modue_name
的形式。- 例如
google.golang.org/grpc
- 例如
背景:Go 程序员已经的问题
- 没有官网的依赖模块管理工具。
- 无奈做到 reproducible builds。
- 次要工具 go get 忽视了用
git tag
的版本号。 - GOPATH 解决不了同一模块的多个版本需要。
- 无奈在 GOPATH 之外写代码。
背景:go get
的问题
-
用该工具拉取代码,太新 且太旧。
- 如果本地没有这个库,会去拉取最新的。
- 如果本地有这个库,则不会更新,太旧。
- Go 程序员偏向于动不动就
go get -u
。 - 每次拉取最新的模块导致的不稳固构建(
low-fidelity builds
)。 - 被墙且不不便用全局代理(这个是被
go env
proxy 解决的,与module
无关)。
没有 go mod
期间的操作
Makefile 外面间接写 go get
(甚至 go get -u
)。
bin:
go get ...
go get ...@...
go build project_path/app_name
这意味着编译的时候须要有网络环境,同时,明天可能编译的代码,今天就有可能失败。
同时,如果批改了某一个内部模块,非得本人 folk 一个,并且放在 git 仓库中。
畏惧依赖模块变动
- 放心通过
go get
获取的内部模块改变影响到本地工程的构建。 - 所以我拷贝到本人的目录下了(不须要感到耻辱,因为 Google 有段时间也是这么干的)。
-
拷贝代码中的门路怎么解决?
- 在最长远期间,须要用脚本来解决被拷贝到工程中的内部模块代码。
- 例如
xx/pkg -> local_dir/xx/pkg
从 2012 年到 2015 年,vendor 计划出炉。
- 主动改代码依赖
Goven
。 - 新工具,解决模块依赖
godep
glide
govendor
。 - 新版本(go 1.5)对 vendor 类计划的官网反对
go vender
dep
。
各种 vendor 实现
- 官网对 GOPATH 打补丁(约定 vendor 作为内部包的文件)。
- 实际中,都把 vendor 一起放入了代码库中。
- 须要有一个版本形容文件。
大部分 vendor 类工具做的事件:
下载指定版本的模块,实现拷贝。
govendor 计划的问题
- 打补丁和降级不方面。
- 代码仓库冗余且俊俏。
- 计算依赖关系的时候,基于 GOPATH 和旧的
go get
算法(对于+external
的模块)。
注: govendor
一类的工具只会去下载最新的版本。并不会计算和抉择特定的版本,仅仅是递归的将
原始工程中的依赖库全副下载下来而已。
问题解决:外围规定
- 模块版本要做向下兼容。
- 不兼容的做大版本解决(不同的大版本不是一个模块)
If an old package and a new package have the same import path,
the new package must be backwards-compatible with the old package.
反过来:如果接口不一样,那么就是不同的包。
然而你不须要保护新的 Git 仓库
-
应用 git tag 来标记和保护。
import "github.com/go-yaml/yaml/v2"
-
查看模块的 tags
$ go list -m -versions github.com/go-yaml/yaml/v2 github.com/go-yaml/yaml/v2 v2.0.0 v2.1.0 v2.1.1 v2.2.0 v2.2.1 v2.2.2 v2.2.3 v2.2.4 v2.2.5 v2.2.6 v2.2.7 v2.2.8 v2.3.0 v2.4.0
思考:如果没有大版本辨别的问题
问题解决办法:
- 大版本用于整体接口降级和变更。
- 两头版本能够减少新接口。
- 小版本修 bug。
参考这一篇极为具体的介绍(具体到啰嗦了)。
问题解决:版本抉择策略
抉择最旧 (小) 的版本而不是最新的。
- 兼容性。(稳固构建方面的思考
reproducible builds
) - Dependency hell. (复杂度和包管理系统的实现上思考 efficient)
须要留神的是:
- 最小抉择的是「版本 」,而不是更新日期!如果包代码没有应用这套规定,那么被当成
0.0.0
版本(pseudo version
)+ 拉取最新的 git 提交,例如v0.0.0-20170915032832-14c0d48ead0c
。 - 对于本地的工程,默认开始创立
go.mod
的时候,依然会去拉取所依赖的模块的最新的版本,见后文「实际操作」。
毁灭 GOPATH
- Go 的版本环境、代码本体的版本和一个隐含的
GOPATH
下的货色决定了编译进去的二进制。 GOPATH
就像是暗藏在 Go 前面的一个尿袋。- 如果代码应用
go mod
来形容所有的依赖模块,那么它位于什么中央就无关紧要了。
两种之前的垃圾版本治理 / 构建策略
[示意图]
- 如果以后环境 (GOPATH) 有这个库,那么不去动他。
go get
go get -u
间接降级到最新。
下面两种都是 low-fidelity builds
最小版本依赖 Minimal version selection
这是作者 Russ Cox 反复强调并且引以为傲的一个概念。
首先咱们要弄清楚,「最小」的意思,是针对曾经写了 go.mod
的模块来说的。
如果本地工程依赖了三个我的项目 a b c,他们都是用了另外一个模块 libx,别离的版本是 1.1 1.2 和 1.3,假如以后 libx 的最新版本曾经是 1.7 了。
如果本地工程不依赖 libx,那么构建时,go mod 会抉择 libx 1.3 作为构建的版本。
可是,如果此时本地工程应用了 libx,此时,创立一个新的 go.mod 并且计算出依赖的话,外面的版本将会是 libx 1.7。同时,在这种场景下,a b c 和本地程序最终都将应用 libx 的 1.7 版本(在一个 Go 程序中,一个大版本的模块只可能有惟一的实体)。
所以,这个「最小版本依赖」,针对的是 a(或者 b c 这种曾经公布的模块)。
应用命令 go version -m ~/go/bin/gopls
能够查看生成的二进制应用的模块版本号。该个性容许你找到适合的环境来从新 build 二进制(用于验证)。
最小版本依赖算法相干的 4 个操作。
- 从构建 build list。
- 降级所有模块。
- 降级指定模块。
- 降级指定模块。
这项特色依赖于「模块的向后兼容性」。如果某个依赖库不听从该规定,那么无奈达到目标。
操作构建 build list
- 找到固定的版本,退出到列表外面,剔掉旧的。
- 用递归的做法来,很慢且在有环的状况下会死循环。
- 图的遍历算法解决。
再来一次 go get
- 用
GOPATH
的状况下,go get
体现和之前一样。 - 用
go mod
的状况下,go get
是用下面提到的办法构建和更新。
思考
- 被本地工程和依赖模块依赖的同一个模块的版本的抉择?最终 build 的时候的后果?
- 批改了第三方模块,然而并不想提交到独立的 git 仓库问题。
- 内网开发的问题。
另一篇:What is Software Engineering?
- Go 的设计思路和决策都来自于对软件工程的思考,会均衡工夫和真正编程中实际。
-
gofmt
的例子:- 要让代码更洁净,对立格调。
- 终止无用的格局相干的探讨。
- 重要:任何中央 copy 来的代码,最终展现进去的样子都是统一的。
- 重要:因为
3.
,所以代码能够被后续的工具解决批改(goimports
gorename
go fix
)。
总得来说,Go 自身就是一门为了工程而打造的语言,他并没有什么相似学术上希图。在最开始的时候,甚至只是为网络服务器程序而设计的。
所以,它肯定是让真正的写理论我的项目工程的人爽的一门语言。
实际
环境变量(GO111MODULE="auto"
)以及「当前目录下是否有 go.mod
文件」决定了 go get
的行为。
对于本地工程间接依赖的版本,go mod tidy
操作,他会去拉取「最新」的版本(依照优先级):
- 最新的稳固 non-prerelease 版本。
- 没有的话,最新的 prerelease 版本。
- 没有的嗯话,最新的 untagged 版本。
在 go.mod
中的标记 indirect
,示意不是本地工程所间接依赖的,通常是由未应用 go.mod
的模块引入静来的(给人一种「编制外」的感觉)的模块。
一个 Go 的模块,有两个规范:
- 是否应用了
semantic verion
- 是否有
go.mod
按理说一个有格调的 Go 模块,都应该依照这两个规范来组织。然而,作为鼎鼎大名的,Google 自家人的 etcd-io/etcd
,却始终都没有应用 go mod
:
这里有一篇吐槽 Etcd 应用 go module 的劫难
如果我的项目外面应用了 etcd 和 go mod
,那么你将面临一大片被拽入到 go.mod
的 github.com/coreos/xxx // indirect
。
这是最新的(2021 年 3 月)的 alpha 版本:
v3.5.0-alpha.0 is an experimental release in order to:
test the "modularized" release process
enable integration testing with the modularized code of 3.5.x.
我试了一下:
go get github.com/coreos/etcd@v3.5.0-alpha.0
go get: github.com/coreos/etcd@v3.5.0-alpha.0: invalid version: module contains a go.mod file, so major version must be compatible: should be v0 or v1, not v3
看来他们还有很远的路要走,在此之前,应用了 etcd 的敌人们,只可能通过在 go.mod
中退出 replace 来给版本依赖关系打补丁。
实际总结
教程中的 cheat sheet:
go mod init creates a new module, initializing the go.mod file that describes it.
go build, go test, and other package-building commands add new dependencies to go.mod as needed.
go list -m all prints the current module’s dependencies.
go get changes the required version of a dependency (or adds a new dependency).
go mod tidy removes unused dependencies.
须要特地留神的是,在 go 1.16 版本外面,go build / go test 等操作 并不 再去批改 go.mod
文件(下面的第二条)!这个行为和之前的版本差别挺大的,不过坦白说,我认为这个改变不赖,防止了很多主动执行的,让人无语的 go.mod
批改。
In Go 1.16, module-aware commands report an error after discovering a problem in go.mod or go.sum instead of attempting to fix the problem automatically. In most cases, the error message recommends a command to fix the problem.
内网开发实际
如果应用了 go.mod
,并且还须要应用一些本地的库,那么能够通过在 go.mod
中应用 replace 来指明:
require hx v0.1.0
replace hx v0.1.0 => /Users/chuanqin/code/go_work/src/hx
同理,咱们对于内网 gitlab 的库,也能够应用同样的办法:
require hx v0.1.0
replace hx v0.1.0 => your_repository/hx