Go Vendor 和 Go Modules:管理和扩展依赖的最佳实践

在Go语言的开发过程中,依赖管理一直是一个至关重要的部分。Go Modules(从Go 1.11版本开始引入)让Go开发者能够更方便地管理项目依赖。而Go Vendor机制则是Go语言的另一个依赖管理方案,特别适用于确保项目中的所有依赖都在项目内部明确管理。本文将深入探讨Go Vendor、Go Modules的使用以及如何扩展依赖管理。

一、Go Vendor机制概述

Go Vendor是Go语言的一种依赖管理方式,它将所有的外部依赖存放到项目的vendor目录中。vendor目录包含了项目所依赖的所有模块的代码副本,确保项目在构建时只使用本地存储的依赖,而不是去外部获取。这种机制有助于避免网络不稳定或不可用时,项目的构建失败。

1.1 GOPATH

在 Go 1.5 版本之前,所有的依赖包都是存放在 GOPATH 下,没有版本控制。这种方式的最大的弊端就是无法实现包的多版本控制,比如项目 A 和项目 B 依赖于不同版本的 package,如果 package 没有做到完全的向前兼容,往往会导致一些问题。

除此之外还会有下面的问题:

  • 如果依赖包持续演进,可能会导致不同开发者在不同时间获取和编译同一个 Go 包时,得到不同的结果,也就是不能保证可重现的构建。
  • 如果依赖包引入了不兼容代码,程序将无法通过编译。
  • 如果依赖包因引入新代码而无法正常通过编译,并且该依赖包的作者又没用及时修复这个问题,这种错误也会传导到你的程序,导致你的程序无法通过编译。

1.2 go vender

Go 1.5版本推出了vendor机制,所谓vendor机制,就是每个项目的根目录下可以有一个vendor目录,里面存放了该项目的依赖的package。go build的时候会先去vendor目录查找依赖,如果没有找到会再去GOPATH目录下查找。

Go编译器会优先感知和使用vendor目录下缓存的第三方包版本,而不是GOPATH环境变量所配置的路径下的第三方包版本。这样,无论第三方依赖包自己如何变化,无论GOPATH环境变量所配置的路径下的第三方包是否存在、版本是什么,都不会影响到Go程序的构建。

如果你将vendor目录和项目源码一样提交到代码仓库,那么其他开发者下载你的项目后,就可以实现可重现的构建。因此,如果使用vendor机制管理第三方依赖包,最佳实践就是将vendor一并提交到代码仓库中。

要想开启vendor机制,你的Go项目必须位于GOPATH环境变量配置的某个路径的src目录下面。如果不满足这一路径要求,那么Go编译器是不会理会Go项目目录下的vendor目录的。

不过 vendor 机制虽然一定程度解决了 Go 程序可重现构建的问题,但对开发者来说,它的体验却不那么好。一方面,Go 项目必须放在 GOPATH 环境变量配置的路径下,庞大的 vendor 目录需要提交到代码仓库,不仅占用代码仓库空间,减慢仓库下载和更新的速度,而且还会干扰代码评审,对实施代码统计等开发者效能工具也有比较大影响。另外,你还需要手工管理 vendor 下面的 Go 依赖包,包括项目依赖包的分析、版本的记录、依赖包获取和存放等等。

Go 1.9 版本推出了实验性质的包管理工具 dep,这里把 dep 归结为 Golang 官方的包管理方式可能有一些不太准确。关于 dep 的争议颇多。

1.3 go mod

Go 1.11 版本推出 modules 机制,简称 mod,更加易于管理项目中所需要的模块。模块是存储在文件树中的 Go 包的集合,其根目录中包含 go.mod 文件。 go.mod 文件定义了模块的模块路径,它也是用于根目录的导入路径,以及它的依赖性要求。每个依赖性要求都被写为模块路径和特定语义版本。

从 Go 1.11 开始,Go 允许在 <math xmlns="http://www.w3.org/1998/Math/MathML"> G O P A T H / s r c 外的任何目录下使用 g o . m o d 创建项目。在 GOPATH/src 外的任何目录下使用 go.mod 创建项目。在 </math>GOPATH/src外的任何目录下使用go.mod创建项目。在GOPATH/src 中,为了兼容性,Go 命令仍然在旧的 GOPATH 模式下运行。从 Go 1.13 开始,go.mod模式将成为默认模式。

2. 设置 GO111MODULE

Go Modules 在 Go 1.11 及 Go 1.12 中有三个模式,根据环境变量 GO111MODULE 定义:

  • 默认模式(未设置该环境变量或 GO111MODULE=auto)

Go 命令行工具在同时满足以下两个条件时使用 Go Modules

  1. 当前目录不在 GOPATH/src/ 下;
  2. 在当前目录或上层目录中存在 go.mod 文件;
  • GOPATH模式(GO111MODULE=off)

Go命令行工具从不使用Go Modules。相反,它查找vendor目录和GOPATH以查找依赖项。

  • Go Modules模式(GO111MODULE=on)

Go 命令行工具只使用 Go Modules,GOPATH不再作为导入目录,但它还是会把下载的依赖储存在 GOPATH/pkg/mod 中,也会把 goinstall的结果放在 GOPATH/bin 中,只移除了 GOPATH/src/。

Go 1.13 默认使用 Go Modules 模式,所以以上内容在 Go 1.13 发布并在生产环境中使用后都可以忽略。

本文以Go 1.13.6为基础详细说明Go modules的使用。

ini 复制代码
# 临时开启 Go modules 功能
export GO111MODULE=on
# 永久开启 Go modules 功能
go env -w GO111MODULE=on

# 设置 Go 的国内代理,方便下载第三方包
go env -w GOPROXY=https://goproxy.cn,direct

逗号后面可以增加多个proxy,最后的direct则是在所有proxy都找不到的时候,直接访问,代理访问不到的私有仓库就可以正常使用了。

其它代理请参考:
studygolang.com/articles/23...

设置后通过 go env 查看如下所示:

ini 复制代码
jiang@jiang-dev:~$ go env
GO111MODULE="on"
.....
GOPROXY="https://goproxy.cn,direct"
.....
jiang@jiang-dev:~$

3. Go module 常用操作

3.1 初始化项目

我们在$GOPATH以外的目录创建一个任意目录,然后初始化go mod init project_name,成功之后会发现目录下会生成一个go.mod文件。

ruby 复制代码
jiang@jiang-dev:~$ mkdir goProject
jiang@jiang-dev:~$ cd goProject/
jiang@jiang-dev:~/goProject$ mkdir apiserver
jiang@jiang-dev:~/goProject$ cd apiserver/
jiang@jiang-dev:~/goProject/apiserver$ go mod init apiserver
go: creating new go.mod: module apiserver
jiang@jiang-dev:~/goProject/apiserver$ ls
go.mod
jiang@jiang-dev:~/goProject/apiserver$

查看内容

ruby 复制代码
jiang@jiang-dev:~/goProject/apiserver$ cat go.mod 
module apiserver

go 1.13
jiang@jiang-dev:~/goProject/apiserver$

go.mod 文件只存在于模块的根目录中。模块子目录的代码包的导入路径等于模块根目录的导入路径(就是前面说的 module path)加上子目录的相对路径。

比如,我们如果创建了一个子目录叫 common,我们不需要(也不会想要)在子目录里面再运行一次 go mod init了,这个代码包会被认为就是 apiserver 模块的一部分,而这个代码包的导入路径就是 apiserver/common。

3.2 添加依赖项

在apiserver文件夹下创建main.go并添加以下内容

go 复制代码
package main
 
import "github.com/gin-gonic/gin"


func ping(c *gin.Context) {
	c.JSON(200, gin.H{
		"message": "pong",
	})
}

func main() {
	r := gin.Default()
	r.GET("/ping", ping)
	r.Run()	// listen and serve on 0.0.0.0:8080
}

执行go build main.go之后会自动下载三方包到默认的目录$GOPATH/pkg/mod,也就是Mod Cache路径。

bash 复制代码
jiang@jiang-dev:~/goProject/apiserver$ go build main.go 
go: finding github.com/gin-gonic/gin v1.7.6
go: downloading github.com/gin-gonic/gin v1.7.6
...
jiang@jiang-dev:~/goProject/apiserver$ ls
go.mod  go.sum  main  main.go
jiang@jiang-dev:~/goProject/apiserver$

进入$GOPATH/pkg/mod目录查看

ruby 复制代码
jiang@jiang-dev:~/goProject/apiserver$ ls $GOPATH/pkg/mod/cache/download/github.com/gin-gonic/gin/@v/
list         v1.7.4.mod      v1.7.5.lock     v1.7.6.info  v1.7.6.ziphash
list.lock    v1.7.4.zip      v1.7.5.mod      v1.7.6.lock
v1.7.4.info  v1.7.4.ziphash  v1.7.5.zip      v1.7.6.mod
v1.7.4.lock  v1.7.5.info     v1.7.5.ziphash  v1.7.6.zip
jiang@jiang-dev:~/goProject/apiserver$

此时再次查看go.mod文件内容

javascript 复制代码
module apiserver

go 1.13

require github.com/gin-gonic/gin v1.7.6 // indirect

其中

  • module 表示模块名称

  • require 依赖包列表以及版本

一般来说,require () 是不需要自己手动去修改的,当运行代码的时候,会根据代码中用到的包自动去下载导入。

  • exclude 禁止依赖包列表,不下载和引用哪些包(仅在当前模块为主模块时生效)

  • replace 替换依赖包列表和引用路径(仅在当前模块为主模块时生效)

replace对于国内开发来说是个神功能,他可以将代码中使用,但国内被墙的代码替换成github上的下载路径,例如:golang.org/x/下的包,全都替换成github地址上的包,版本使用latest即可

bash 复制代码
replace (
	golang.org/x/net => github.com/golang/net latest
	golang.org/x/tools => github.com/golang/tools latest
	golang.org/x/crypto => github.com/golang/crypto latest
	golang.org/x/sys => github.com/golang/sys latest
	golang.org/x/text => github.com/golang/text latest
	golang.org/x/sync => github.com/golang/sync latest
)

indirect表示这个库是间接引用进来的。

使用go list -m all可以查看到所有依赖列表,也可以使用go list -json -m all输出json格式的打印结果。

bash 复制代码
jiang@jiang-dev:~/goProject/apiserver$ go list -m all
apiserver
github.com/davecgh/go-spew v1.1.1
github.com/gin-contrib/sse v0.1.0
...
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405
gopkg.in/yaml.v2 v2.2.8
jiang@jiang-dev:~/goProject/apiserver$

除了go.mod之外,go命令行工具还维护了一个go.sum文件,它包含了指定的模块的版本内容的哈希值作为校验参考:

bash 复制代码
jiang@jiang-dev:~/goProject/apiserver$ cat go.sum 
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=

go命令行工具使用go.sum文件来确保你的项目依赖的模块不会发生变化------无论是恶意的,还是意外的,或者是其它的什么原因。go.mod文件和go.sum文件都应该保存到你的代码版本控制系统里面去。

go.sum 这个文件记录了源码的直接依赖和间接依赖包的相关版本的 hash 值,用来校验本地包的真实性。在构建的时候,如果本地依赖包的 hash 值与 go.sum 文件中记录的不一致,就会被拒绝构建,这样可以确保你的项目所依赖的 module 内容,不会被恶意或意外篡改。

3.3 回退版本

将GIN框架的版本回退到上个版本。这里需要使用一个命令查看依赖的版本历史。

bash 复制代码
jiang@jiang-dev:~/goProject/apiserver$ go list -m -versions github.com/gin-gonic/gin
github.com/gin-gonic/gin v1.1.1 v1.1.2 v1.1.3 v1.1.4 v1.3.0 v1.4.0 v1.5.0 v1.6.0 v1.6.1 v1.6.2 v1.6.3 v1.7.0 v1.7.1 v1.7.2 v1.7.3 v1.7.4 v1.7.5 v1.7.6
jiang@jiang-dev:~/goProject/apiserver$

将版本回退到指定版本有两种方法:

  1. 使用 go get 命令
bash 复制代码
# 只需要在依赖后面加上 @version 就可以了
jiang@jiang-dev:~/goProject/apiserver$ go get github.com/gin-gonic/gin@v1.7.5	
go: finding github.com/gin-gonic/gin v1.7.5
go: downloading github.com/gin-gonic/gin v1.7.5
go: extracting github.com/gin-gonic/gin v1.7.5
go: downloading github.com/json-iterator/go v1.1.9
go: extracting github.com/json-iterator/go v1.1.9
......

请注意我们给 go get 命令的参数后面显式地指定了 @v1.7.5 ,事实上每个传递给 go get 的参数都能在后面显式地指定一个版本号,默认情况下这个版本号是 @latest,这代表 Go 命令行工具会尝试下载最新的版本。

查看回退之后的版本

bash 复制代码
jiang@jiang-dev:~/goProject/apiserver$ go list -m all
apiserver
github.com/gin-gonic/gin v1.7.5
  1. 使用go mod命令
ruby 复制代码
jiang@jiang-dev:~/goProject/apiserver$ go mod edit -require="github.com/gin-gonic/gin@v1.7.4"
jiang@jiang-dev:~/goProject/apiserver$ go get
go: downloading github.com/gin-gonic/gin v1.7.4
go: extracting github.com/gin-gonic/gin v1.7.4
go: finding github.com/gin-gonic/gin v1.7.4
jiang@jiang-dev:~/goProject/apiserver$ 

jiang@jiang-dev:~/goProject/apiserver$ go list -m all
apiserver
....
github.com/gin-gonic/gin v1.7.4

在Go Module构建模式下,当依赖的主版本号为 0 或 1 的时候,我们在Go源码中导入依赖包,不需要在包的导入路径上增加版本号,也就是:

arduino 复制代码
import github.com/user/repo/v0 等价于 import github.com/user/repo
import github.com/user/repo/v1 等价于 import github.com/user/repo

3.4 升级版本

升级版本和回退版本使用的命令一样,只是后面的版本号不同,不再额外说明

3.5 删除未使用的依赖项

我们在构建一个代码包的时候(比如说 go build 或者 go test),可以轻易的知道哪些依赖缺失,从而将它自动添加进来,但很难知道哪些依赖可以被安全的移除掉。移除一个依赖项需要在检查完模块中所有代码包和这些代码包的所有可能的编译标签的组合。一个普通的 build 命令不会获得这么多的信息,所以它不能保证安全地移除掉没用的依赖项。

可以用go mod tidy命令来清除这些没用到的依赖项:

go 复制代码
go mod tidy

查看 go mod 命令选项

sql 复制代码
jiang@jiang-dev:~/goProject/apiserver$ go mod
Go mod provides access to operations on modules.

Note that support for modules is built into all the go commands,
not just 'go mod'. For example, day-to-day adding, removing, upgrading,
and downgrading of dependencies should be done using 'go get'.
See 'go help modules' for an overview of module functionality.

Usage:

	go mod <command> [arguments]

The commands are:

	download    download modules to local cache		# 下载依赖的module到本地cache
	edit        edit go.mod from tools or scripts	# 编辑go.mod文件
	graph       print module requirement graph		# 打印模块依赖图
	init        initialize new module in current directory	# 在当前文件夹下初始化一个新的module, 创建go.mod文件
	tidy        add missing and remove unused modules	# 增加丢失的module,去掉未用的module
	vendor      make vendored copy of dependencies	# 将依赖复制到vendor下
	verify      verify dependencies have expected content	# 校验依赖
	why         explain why packages or modules are needed	# 解释为什么需要依赖

Use "go help mod <command>" for more information about a command.

go mod tidy命令会扫描Go源码,并自动找出项目依赖的外部Go Module以及版本,下载这些依赖并更新本地的go.mod文件。

由 go mod tidy 下载的依赖 module 会被放置在本地的 module 缓存路径下,默认值为 $GOPATH/pkg/mod,Go 1.15 及以后版本可以通过 GOMODCACHE 环境变量,自定义本地 module 的缓存路径。

3.6 引入主版本号大于 1 的三方库

语义导入版本机制有一个原则:如果新旧版本的包使用相同的导入路径,那么新包与旧包是兼容的。也就是说,如果新旧两个包不兼容,那么我们就应该采用不同的导入路径

按照语义版本规范,如果我们要为项目引入主版本号大于 1 的依赖,比如v2.0.0,那么由于这个版本与v1、v0开头的包版本都不兼容,我们在导入v2.0.0包时,就要使用像下面代码中那样不同的包导入路径:

bash 复制代码
import github.com/user/repo/v2/xxx

也就是说,如果我们要为Go项目添加主版本号大于 1 的依赖,我们就需要使用"语义导入版本"机制,在声明它的导入路径的基础上,加上版本号信息。首先,我们在源码中,以空导入的方式导入 v7 版本的 github.com/go-redis/redis 包:

go 复制代码
package main

import (
  _ "github.com/go-redis/redis/v7" // "_"为空导入
  "github.com/google/uuid"
  "github.com/sirupsen/logrus"
)

func main() {
  logrus.Println("hello, go module mode")
  logrus.Println(uuid.NewString())
}

我们通过go get获取redis的v7版本:

go 复制代码
$go get github.com/go-redis/redis/v7
go: downloading github.com/go-redis/redis/v7 v7.4.1
go: downloading github.com/go-redis/redis v6.15.9+incompatible
go get: added github.com/go-redis/redis/v7 v7.4.1

3.7 特殊情况:使用 vendor

vendor机制虽然诞生于GOPATH构建模式主导的年代,但在Go Module构建模式下,它依旧被保留了下来,并且成为了Go Module构建机制的一个很好的补充。特别是在一些不方便访问外部网络,并且对Go应用构建性能敏感的环境。

和GOPATH构建模式不同,Go Module构建模式下,我们再也无需手动维护vendor目录下的依赖包了,Go提供了可以快速建立和更新vendor的命令,我们还是以前面的module-mode项目为例,通过下面命令为该项目建立vendor:

bash 复制代码
$go mod vendor
$tree -LF 2 vendor
vendor
├── github.com/
│   ├── google/
│   ├── magefile/
│   └── sirupsen/
├── golang.org/
│   └── x/
└── modules.txt

我们看到,go mod vendor命令在vendor目录下,创建了一份这个项目的依赖包的副本,并且通过vendor/modules.txt记录了vendor下的module以及版本。

如果我们要基于 vendor 构建,而不是基于本地缓存的 Go Module 构建,我们需要在 go build 后面加上 -mod=vendor 参数。在 Go 1.14 及以后版本中,如果 Go 项目的顶层目录下存在 vendor 目录,那么 go build 默认也会优先基于 vendor 构建,除非你给 go build 传入 -mod=mod 的参数。

通常我们直接使用go module(非vendor) 模式即可满足大部分需求。如果是那种开发环境受限,因无法访问外部代理而无法通过go命令自动解决依赖和下载依赖的环境下,我们通过vendor来辅助解决。

4. go mod 工作机制

4.1 go mod 版本表达方式

go mod 有两种版本表达方式,分别为语义化版本和基于某一个 commit 的伪版本。

4.2 管理 go mod 命令

注意:

  • go get 默认不会将语义化版本修改为伪版本
  • @latest表示最新的语义化版本,而不是伪版本
  • -u 表示更新该依赖并同时更新该依赖中所有参与编译的依赖到 minor 版本

4.3 版本选择算法 Minimal Version Selection(MVS)

下面问题的答案选择 B

myproject 有两个直接依赖 A 和 B,A 和 B 有一个共同的依赖包 C,但 A 依赖 C 的 v1.1.0 版本,而 B 依赖的是 C 的 v1.3.0 版本,并且此时 C 包的最新发布版为 v1.7.0。这个时候,Go 命令是如何为 myproject 选出间接依赖包 C 的版本呢?选出的究竟是 v1.7.0、v1.1.0 还是 v1.3.0 呢?

Go会在该项目依赖项的所有版本中,选出符合项目整体要求的"最小版本"。这个例子中,C v1.3.0是符合项目整体要求的版本集合中的版本最小的那个,于是Go命令选择了C v1.3.0,而不是最新最大的C v1.7.0。

如果现在B依赖的不是v1.3.0而是v2.3.0, 那最终go会选择C v1.1.0和C v2.3.0,这两个都会下载到本地并链接到最终程序中。因为根据go的语义导入版本的规则,不同major号的module就是不同的module。

go module采用的语义版本导入机制,v1.0.0和v2.0.0的major号不同,是两个完全不同的版本,是可以共同被某个包同时导入的,语法如下;

go 复制代码
import (
    "c"
    "c/v2"
)

5. 常见问题

5.1 导入本地创建的 module

如何import自己在本地创建的module,在这个module还没有发布到GitHub的情况下?

假如你的module a要import的module b将发布到github.com/user/repo中,那么你可以手动在module a的go.mod中的require块中手工加上一条:

bash 复制代码
require github.com/user/repo v1.0.0

注意v1.0.0这个版本号是一个临时的版本号,目的是满足go.mod中require块的语法要求。

然后在module a的go.mod中使用replace将上面对module b的require替换为本地的module b:

ini 复制代码
replace github.com/user/repo v1.0.0 => module b本地路径

这样go命令就会使用你本地正在开发、尚未提交github的module b了。

常用工具和方法

  • 在使用 go get 更新时,如果不需要更新依赖的依赖则不要使用 -u 参数,如果使用该参数会使得依赖的依赖更新,有可能导致由于依赖的依赖版本不兼容而编译失败。
  • 有些工程不使用 go mod 原因有两个: 一是在 go mod 推广前其版本号已经大于 1;二是便于使用 go get 直接拉取到 V2 以上的版本。

五、总结

Go Modules和Go Vendor机制为Go开发者提供了强大的依赖管理工具。Go Modules简化了模块管理,提供了更高效、更可靠的依赖下载方式,而Go Vendor则适用于要求离线构建和依赖管理明确的场景。在实际开发中,开发者可以根据项目需求选择合适的依赖管理方式,并合理结合Go Modules与Go Vendor,确保项目的稳定性和可维护性。

通过灵活的模块管理、依赖版本控制和清理未使用的依赖,Go开发者可以提升代码的可移植性、可维护性,并确保项目的长期稳定运行。

相关推荐
bobz96525 分钟前
linux cpu CFS 调度器有使用 令牌桶么?
后端
bobz96529 分钟前
linux CGROUP CPU 限制有使用令牌桶么?
后端
David爱编程1 小时前
多核 CPU 下的缓存一致性问题:隐藏的性能陷阱与解决方案
java·后端
追逐时光者1 小时前
一款基于 .NET 开源、功能全面的微信小程序商城系统
后端·.net
阿巴~阿巴~2 小时前
Git 删除文件
git·gitee·github
绝无仅有2 小时前
Go 并发同步原语:sync.Mutex、sync.RWMutex 和 sync.Once
后端·面试·github
自由的疯3 小时前
Java 实现TXT文件导入功能
java·后端·架构
现在没有牛仔了3 小时前
SpringBoot实现操作日志记录完整指南
java·spring boot·后端
国家不保护废物3 小时前
10万条数据插入页面:从性能优化到虚拟列表的终极方案
前端·面试·性能优化