作者:尹正杰
版权声明:原创作品,谢绝转载!否则将追究法律责任。
一.Go Modules发展史
1.前言
一般编程语言都会提供依赖库管理工具,例如python的pip、node.js的npm,java的maven,rust的cargo,Go语言也有提供自己的依赖库管理工具。
Go语言在1.11提出了Go mod,每次版本或多或少都会对go.mod进行改进优化,go mod也越来越好,当前大多数公司都使用go mod来管理依赖库。
考虑新手很容易在"go module"使用上犯迷糊,本篇文章主要介绍"Go Modules"发展史及快速入门的必要知识点。
2.早期第三方包存储在GOPATH路径
起初Go语言在1.5之前没有依赖管理工具,若想引入依赖库,需要执行go get命令将代码拉取放入GOPATH/src目录下。
这样的依赖管理方式存在一个致命的缺陷,那就是不支持版本管理,同一个依赖包只能存在一个版本的代码。
可是我们本地的多个项目完全可能分别依赖同一个第三方包的不同版本。
3.vendor阶段
为了解决隔离项目的包依赖问题,Go1.5版本推出了vendor机制,环境变量中有一个GO15VENDOREXPERIMENT需要设置为1,该环境变量在Go1.6版本时变成默认开启,目前已经退出了历史舞台;
vendor其实就是将原来放在"GOPATH/src"的依赖包放到工程的vendor目录中进行管理,不同工程独立地管理自己的依赖包,相互之间互不影响,原来是包共享的模式,通过vendor这种机制进行隔离,在项目编译的时候会先去vendor目录查找依赖,如果没有找到才会再去GOPATH目录下查找。
vendor阶段的优劣势:
优点:
保证了功能项目的完整性,减少了下载依赖包,直接使用vendor就可以编译
缺点:
仍然没有解决版本控制问题,go get仍然是拉取最新版本代码。
4.社区管理工具层出不穷
很多优秀的开发者在这期间也都实现了不错的包依赖管理工具,例如:
godep:
https://github.com/tools/godep
govendor:
https://github.com/kardianos/govendor
glide:
https://github.com/Masterminds/glide
dep:
https://github.com/golang/dep
dep应该是其中最成功的,得到了Go语言官方的支持,该项目也被放到了https://github.com/golang/dep,但是为什么dep没有称为官宣的依赖工具呢?
其实因为随着"Russ Cox"与"Go团队"中的其他成员不断深入地讨论,发现dep的一些细节似乎越来越不适合Go,因此官方采取了另起"proposal(建议)"的方式来推进,其方案的结果一开始先是释出”vgo“,最终演变为我们现在所见到的”Go modules“;
5.go modules官宣官方管理工具
go modules是"Russ Cox"(Go语言核心开发团队人员之一)推出来的,发布于Go1.11。
go modules成长于Go1.12,丰富于Go1.13,正式于Go1.14推荐在生产上使用。
go modules几乎后续的每个版本都或多或少的有一些优化,比如在Go1.16引入go mod retract、在Go1.18引入go work工作区的概念。
参考连接:
https://developer.aliyun.com/article/1604714
二.go module介绍
1.GO111MODULE环境变量
这个环境变量是"Go Modules"的开关,主要有以下参数:
auto:
只在项目包含了go.mod文件时启动go modules,在Go1.13版本中是默认值。
on:
无脑启动Go Modules,推荐设置,Go1.14版本以后的默认值。
off:
禁用Go Modules,一般没有使用go modules的工程使用。
我现在使用的Go版本是1.22.4,默认GO111MODULE=on。
2.GOPROXY
这个环境变量主要是用于设置Go模块代理"Go module proxy",其作用是用于使Go在后续拉取模块版本时能够脱离传统的VCS方式,直接通过镜像站点来快速拉取。
GOPROXY的默认值是"https://proxy.golang.org,direct",由于某些原因国内无法正常访问该地址,所以我们通常需要配置一个可访问的地址。目前社区使用比较多的有两个"https://goproxy.cn"和"https://goproxy.io",当然如果你的公司有提供GOPROXY地址那么就直接使用。
设置GOPAROXY的命令如下:
# go env -w GOPROXY=https://goproxy.cn,direct
GOPROXY允许设置多个代理地址,多个地址之间需使用英文逗号","分隔。最后的"direct"是一个特殊指示符,用于指示Go回源到源地址去抓取(比如"GitHub"等)。
当配置有多个代理地址时,如果第一个代理地址返回"404"或"410"错误时,Go会自动尝试下一个代理地址,当遇见"direct"时触发回源,也就是回到源地址去抓取。
3.GOSUMDB
该环境变量的值是一个Go checksum database,用于保证Go在拉取模块版本时拉取到的模块版本数据未经篡改。
若发现不一致会中止,也可以将值设置为off即可以禁止Go在后续操作中校验模块版本;
什么是Go checksum database?
Go checksum database主要用于保护Go不会从任何拉到被篡改过的非法Go模块版本,详细算法机制可以看一下:
https://go.googlesource.com/proposal/+/master/design/25530-sumdb.md#proxying-a-checksum-database
GOSUMDB的默认值是sum.golang.org,默认值与自定义值的格式不一样,默认值在国内是无法访问,这个值我们一般不用动,因为我们一般已经设置好了GOPROXY,goproxy.cn支持代理sum.golang.org;
GOSUMDB的值自定义格式如下:
- 格式1:
<SUMDB_NAME>+<PUBLIC_KEY>。
- 格式2:
<SUMDB_NAME>+<PUBLIC_KEY> <SUMDB_URL>。
4.GONOPROXY/GONOSUMDB/GOPRIVATE
这三个环境变量放在一起说,一般在项目中不经常使用,这三个环境变量主要用于私有模块的拉取。
在GOPROXY、GOSUMDB中无法访问到模块的场景中,例如拉取git上的私有仓库。
GONOPROXY、GONOSUMDB的默认值是GOPRIVATE的值,所以我们一般直接使用GOPRIVATE即可,其值也是可以设置多个,以英文逗号进行分割。
"GOPRIVATE"用来告诉"go"命令哪些仓库属于私有仓库,不必通过代理服务器拉取和校验。
"GOPRIVATE"的值也可以设置多个,多个地址之间使用英文逗号","分隔。我们通常会把自己公司内部的代码仓库设置到"GOPRIVATE"中,例如:
$ go env -w GOPRIVATE="git.yinzhengjie.com"
这样在拉取以"git.yinzhengjie.com"为路径前缀的依赖包时就能正常拉取了。
此外,如果公司内部自建了"GOPROXY"服务,那么我们可以通过设置"GONOPROXY=none",允许从内部代理拉取私有仓库的包。
也可以使用通配符的方式进行设置,对域名设置通配符号,这样子域名就都不经过Go module proxy和Go checksum database;
5.go.mod文件
go.mod是启用Go modules的项目所必须且最重要的文件,其描述了当前项目的元信息,每个go.mod文件开头符合包含如下信息:
module:
用于定义当前项目的模块路径(突破$GOPATH路径)。
go:
当前项目Go版本,目前只是标识作用、
require:
用设置一个特定的模块版本,可以指定需要依赖的版本号。
Go modules中建议使用语义化版本控制,其建议的版本号格式如下:
主版本号:
发布了不兼容的版本迭代时递增(breaking changes)。
次版本号:
发布了功能性更新时递增。
修订号:
发布了bug修复类更新时递增。
exclude:
用于从使用中排除一个特定的模块版本。
replace:
用于将一个模块版本替换为另外一个模块版本。
比如将一个无法访问的域名替换成国内的镜像站点或者本地路径均可。
retract:
用来声明该第三方模块的某些发行版本不能被其他模块使用,在Go1.16引入。
如果某个发布的版本存在致命缺陷不再想让用户使用时,我们可以使用retract声明废弃的版本。
indirect:
行尾的indirect表示该依赖包为间接依赖,说明在当前程序中的所有"import"语句中没有发现引入这个包。
6.go.sum文件
使用go module下载了依赖后,项目目录下还会生成一个go.sum文件。
这个文件中详细记录了当前项目中引入的依赖包的信息及其hash 值。go.sum文件内容通常是以类似下面的格式出现。
- <module> <version>/go.mod <hash>
- <module> <version> <hash>
- <module> <version>/go.mod <hash>
不同于其他语言提供的基于中心的包管理机制,例如“npm”和“pypi”等,Go并没有提供一个中央仓库来管理所有依赖包,而是采用分布式的方式来管理包。为了防止依赖包被非法篡改,Go module引入了go.sum机制来对依赖包进行校验。
7."go mod"依赖保存位置
"go mod download"会将依赖缓存到本地,Go module缓存的目录是"$GOPATH/pkg/mod/cache“、"GOPATH/pkg/sum"。
这些缓存依赖可以被多个项目使用,未来可能会迁移到$GOCACHE下面。
每个依赖包都会带有版本号进行区分,这样就允许在本地存在同一个包的多个不同版本。
温馨提示:
如果想清除所有本地已缓存的依赖包数据,可以执行"go clean -modcache"命令。
三. go module常用命令
1."go mod"常用的子命令概述
命令 | 介绍 |
---|---|
go mod init | 初始化项目依赖,生成go.mod文件 |
go mod download | 根据go.mod文件下载依赖 |
go mod tidy | 比对项目文件中引入的依赖与go.mod进行比对 |
go mod graph | 输出依赖关系图 |
go mod edit | 编辑go.mod文件 |
go mod vendor | 将项目的所有依赖导出至vendor目录 |
go mod verify | 检验一个依赖包是否被篡改过 |
go mod why | 解释为什么需要某个依赖 |
Go module 是 Go1.11 版本发布的依赖管理方案,从 Go1.14 版本开始推荐在生产环境使用,于Go1.16版本默认开启。
如上表所示,Go module 提供了以上命令供我们使用,我们可以使用“go help mod”查看可以使用的命令。
Go语言在"go module"的过渡阶段提供了"GO111MODULE"这个环境变量来作为是否启用"go module"功能的开关。
考虑到Go1.16之后 "go module"已经默认开启,所以本书不再介绍该配置,对于刚接触Go语言的读者而言完全没有必要了解这个历史包袱。
2.使用go module引入包第三方包
2.1 初始化项目
1.初始化项目
yinzhengjie@bogon 02-crm % pwd
/Users/yinzhengjie/golang/gosubjects/src/gocode/devops/05-package/02-crm
yinzhengjie@bogon 02-crm %
yinzhengjie@bogon 02-crm % ls -l
total 0
yinzhengjie@bogon 02-crm %
yinzhengjie@bogon 02-crm % go mod init web-server
go: creating new go.mod: module web-server
yinzhengjie@bogon 02-crm %
yinzhengjie@bogon 02-crm % ls -l
total 8
-rw-r--r--@ 1 yinzhengjie staff 29 7 14 23:00 go.mod
yinzhengjie@bogon 02-crm %
yinzhengjie@bogon 02-crm % cat go.mod
module web-server
go 1.22.4
yinzhengjie@bogon 02-crm %
2.go.mod内容说明:
module web-server:
定义当前项目的导入路径。
go 1.22.4:
标识当前项目使用的go版本。
温馨提示:
go.mod文件会记录项目使用的第三方依赖包信息,包括包名和版本,由于我们的"web-server"项目目前还没有使用到第三方依赖包,所以go.mod文件暂时还没有记录任何依赖包信息,只有当前项目的一些信息。
2.2 下载第三方程序包
1.安装gin框架
yinzhengjie@bogon 02-crm % go get -u github.com/gin-gonic/gin
2.查看本地
yinzhengjie@bogon 02-crm % ls -l
total 24
-rw-r--r--@ 1 yinzhengjie staff 1376 7 14 23:17 go.mod # 依赖文件
-rw-r--r--@ 1 yinzhengjie staff 7028 7 14 23:17 go.sum # 依赖包的校验文件
yinzhengjie@bogon 02-crm %
yinzhengjie@bogon 02-crm % cat go.mod # 关于go.mod的文件内容解析请参考上文中的介绍哟~
module web-server
go 1.22.4
require (
github.com/bytedance/sonic v1.11.9 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.4 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.10.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.22.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
yinzhengjie@bogon 02-crm %
温馨提示:
1.在执行go get命令下载一个新的依赖包时一般会额外添加"-u"参数,强制更新现有依赖;
2.行尾的indirect表示该依赖包为间接依赖,说明在当前程序中的所有"import"语句中没有发现引入这个包;
3.下载软件包的名称和版本使用"@"区分,若不指定版本则默认会下载最新的发布版本;
3.代码测试
3.1 项目组织结构
如上图所示,是我随意找的目录,做的目录组织结构,其中包括引入第三方包和本项目中自定义包引入。
3.2 blog.go源代码
package blog
import (
// 导入第三方仓库代码
"github.com/gin-gonic/gin"
)
func StartWebServer() {
// 创建一个默认的路由引擎
r := gin.Default()
// GET:请求方式;/hello:请求的路径
// 当客户端以GET方法请求/hello路径时,会执行后面的匿名函数
r.GET("/hello", func(c *gin.Context) {
// c.JSON:返回JSON格式的数据
c.JSON(200, gin.H{
"name": "尹正杰",
"blog": "https://www.cnblogs.com/yinzhengjie",
})
})
// 启动HTTP服务,默认在0.0.0.0:8080启动服务
r.Run()
}
3.3 main.go源代码
package main
import (
// 注意,此处的"web-server"是"go.mod文件中记录的"module web-server"对应的名称哟~
// 而"web-server"下对应的"blog"是我们自定义的包名哟~
"web-server/blog"
)
func main() {
blog.StartWebServer()
}
3.4 运行项目并访问WebUI
如上图所示,我们可以正常访问咱们自定义的项目啦~