参考
一、总览
go.mod
文件中通过指令
声明module信息,用于控制命令行工具进行版本选择。一共有四个指令可供使用:
- module: 声明module名称;
- require: 声明依赖以及其版本号;
- replace: 替换require中声明的依赖,使用另外的依赖及其版本号;
- exclude: 禁用指定的依赖;
其中module
和require
我们前面已介绍过,module
用于指定module的名字,如module github.com/renhongcai/gomodule
,那么其他项目引用该module时其import路径需要指定github.com/renhongcai/gomodule
。require
用于指定依赖,如require github.com/google/uuid v1.1.1
,该指令相当于告诉go build
使用github.com/google/uuid
的v1.1.1
版本进行编译。
本节开始介绍replace
的用法,包括其工作机制和常见的使用场景,下一节再对exclude
展开介绍。
# replace 概览
# 场景1:replace 偷梁换柱,替换为别的版本
[root@ecs-d8b6 gomodule]# cat go.mod
module github.com/renhongcai/gomodule
go 1.13
require github.com/google/uuid v1.1.1 # 注意此处声明使用 1.1.1 版本
replace github.com/google/uuid v1.1.1 => github.com/google/uuid v1.1.0 # 但实际使用的是 1.1.0 版本,因为可能觉得 1.1.1 版本不好用,因此偷梁换柱
# 场景2:replace 引入本地包,进行依赖调试和测试
require github.com/google/uuid v1.1.1
replace (
github.com/google/uuid v1.1.1 => ../uuid # 本地路径,可以使用绝对路径或相对路径
)
# 场景3:replace 替换不可下载的包,换为其他镜像源
require (
golang.org/x/text v0.3.2 # 假设目前此包无法下载
)
replace golang.org/x/text v0.3.2 => github.com/golang/text v0.3.2 # 替换为其他可用的包,镜像源(功能都一致)
# 场景4:使用 fork 仓库
# 假设目前 uuid 开源包 v1.1.1 发现重大bug,此时我们将其 fork 进行 bug 修复,之后替换为我们修复后的版本
# 注意 开源仓库修复后,最好还是改为开源仓库地址
github.com/google/uuid v1.1.1 => github.com/RainbowMango/uuid v1.1.2
# 场景5:禁止被依赖情况
# k8s 不希望【自己整体】被外部引用,希望外部引用时采用组件方式
# 因此,k8s 的 mod 标记所有版本 v0.0.0
# 但 k8s 内部也不认识呀,怎么办? —— 采用 replace,替换为可用的
# 但是外部 k8s 整体包的时候,不也是具有 replace 吗? —— 有是有,但是他们不认识
# 【外部引用只会引用 require部分,忽略replace部分】,这样外部就只能看到 v0.0.0 版本,但就是找不到相关的包
# `replace`指令在当前模块不是`main module`时会被自动忽略的,Kubernetes正是利用了这一特性来实现对外隐藏依赖版本号来实现禁止直接引用的目的。
module k8s.io/kubernetes
require (
...
k8s.io/api v0.0.0
k8s.io/apiextensions-apiserver v0.0.0
k8s.io/apimachinery v0.0.0
k8s.io/apiserver v0.0.0
k8s.io/cli-runtime v0.0.0
k8s.io/client-go v0.0.0
k8s.io/cloud-provider v0.0.0
...
)
replace (
k8s.io/api => ./staging/src/k8s.io/api
k8s.io/apiextensions-apiserver => ./staging/src/k8s.io/apiextensions-apiserver
k8s.io/apimachinery => ./staging/src/k8s.io/apimachinery
k8s.io/apiserver => ./staging/src/k8s.io/apiserver
k8s.io/cli-runtime => ./staging/src/k8s.io/cli-runtime
k8s.io/client-go => ./staging/src/k8s.io/client-go
k8s.io/cloud-provider => ./staging/src/k8s.io/cloud-provider
)
二、replace 工作机制
顾名思义,replace
指替换,它指示编译工具替换require
指定中出现的包,比如,我们在require
中指定的依赖如下:
module github.com/renhongcai/gomodule
go 1.13
require github.com/google/uuid v1.1.1
此时,我们可以使用go list -m all
命令查看最终选定的版本:
[root@ecs-d8b6 gomodule]# go list -m all
github.com/renhongcai/gomodule
github.com/google/uuid v1.1.1
毫无意外,最终选定的uuid版本正是我们在require中指定的版本v1.1.1
。
如果我们想使用uuid的v1.1.0版本进行构建,可以修改require指定,还可以使用replace来指定。 需要说明的是,正常情况下不需要使用replace来修改版本,最直接的办法是修改require即可,虽然replace也能够做到,但这不是replace的一般使用场景。 下面我们先通过一个简单的例子来说明replace的功能,随即介绍几种常见的使用场景。
比如,我们修改go.mod
,添加replace指令:
[root@ecs-d8b6 gomodule]# cat go.mod
module github.com/renhongcai/gomodule
go 1.13
require github.com/google/uuid v1.1.1 # 注意此处声明使用 1.1.1 版本
replace github.com/google/uuid v1.1.1 => github.com/google/uuid v1.1.0 # 但实际使用的是 1.1.0 版本,因为可能觉得 1.1.1 版本不好用,因此偷梁换柱
replace github.com/google/uuid v1.1.1 => github.com/google/uuid v1.1.0
指定表示替换uuid v1.1.1版本为 v1.1.0,此时再次使用go list -m all
命令查看最终选定的版本:
[root@ecs-d8b6 gomodule]# go list -m all
github.com/renhongcai/gomodule
github.com/google/uuid v1.1.1 => github.com/google/uuid v1.1.0
可以看到其最终选择的uuid版本为 v1.1.0。如果你本地没有v1.1.0版本,你或许还会看到一条go: finding github.com/google/uuid v1.1.0
信息,它表示在下载uuid v1.1.0包,也从侧面证明最终选择的版本为v1.1.0。
到此,我们可以看出replace
的作用了,它用于替换require
中出现的包,它正常工作还需要满足两个条件:
第一,replace
仅在当前module为main module
时有效,比如我们当前在编译github.com/renhongcai/gomodule
,此时就是main module
,如果其他项目引用了github.com/renhongcai/gomodule
,那么其他项目编译时,replace
就会被自动忽略。
第二,replace
指定中=>
前面的包及其版本号必须出现在require
中才有效,否则指令无效,也会被忽略。 比如,上面的例子中,我们指定replace github.com/google/uuid => github.com/google/uuid v1.1.0
,或者指定replace github.com/google/uuid v1.0.9 => github.com/google/uuid v1.1.0
,二者均都无效。
三、 replace 使用场景
前面的例子中,我们使用replace
替换require
中的依赖,在实际项目中replace
在项目中经常被使用,其中不乏一些精彩的用法。 但不管应用在哪种场景,其本质都一样,都是替换require
中的依赖。
3.1 替换无法下载的包
由于中国大陆网络问题,有些包无法顺利下载,比如golang.org
组织下的包,值得庆幸的是这些包在GitHub都有镜像,此时 就可以使用GitHub上的包来替换。
比如,项目中使用了golang.org/x/text
包:
package main
import (
"fmt"
"github.com/google/uuid"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
func main() {
id := uuid.New().String()
fmt.Println("UUID: ", id)
p := message.NewPrinter(language.BritishEnglish)
p.Printf("Number format: %v.\n", 1500)
p = message.NewPrinter(language.Greek)
p.Printf("Number format: %v.\n", 1500)
}
上面的简单例子,使用两种语言language.BritishEnglish
和language.Greek
分别打印数字1500
,来查看不同语言对数字格式的处理,一个是1,500
,另一个是1.500
。此时就会分别引入"golang.org/x/text/language"
和"golang.org/x/text/message"
。
执行go get
或go build
命令时会就再次分析依赖情况,并更新go.mod
文件。网络正常情况下,go.mod
文件将会变成下面的内容:
module github.com/renhongcai/gomodule
go 1.13
require (
github.com/google/uuid v1.1.1
golang.org/x/text v0.3.2
)
replace github.com/google/uuid v1.1.1 => github.com/google/uuid v1.1.0
我们看到,依赖golang.org/x/text
被添加到了require中。(多条require语句会自动使用()
合并)。此外,我们没有刻意指定golang.org/x/text
的版本号,Go命令行工具根据默认的版本计算规则使用了 v0.3.2版本,此处我们暂不关心具体的版本号。
没有合适的网络代理情况下,
golang.org/x/text
很可能无法下载。那么此时,就可以使用replace
来让我们的项目使用GitHub上相应的镜像包。我们可以添加一条新的replace
条目,如下所示:
replace (
github.com/google/uuid v1.1.1 => github.com/google/uuid v1.1.0
golang.org/x/text v0.3.2 => github.com/golang/text v0.3.2
)
此时,项目编译时就会从GitHub下载包。我们源代码中import路径 golang.org/x/text/xxx
不需要改变。
也许有读者会问,是否可以将import路径由golang.org/x/text/xxx
改成github.com/golang/text/xxx
?这样一来,就不需要使用replace来替换包了。
遗憾的是,不可以。因为github.com/golang/text
只是镜像仓库,其go.mod
文件中定义的module还是module golang.org/x/text
,这个module名字直接决定了你的import的路径。
3.2 调试依赖包
有时我们需要调试依赖包,此时就可以使用replace
来修改依赖,如下所示:
replace (
github.com/google/uuid v1.1.1 => ../uuid
golang.org/x/text v0.3.2 => github.com/golang/text v0.3.2
)
语句
github.com/google/uuid v1.1.1 => ../uuid
使用本地的uuid来替换依赖包,此时,我们可以任意地修改../uuid
目录的内容来进行调试。
除了使用相对路径,还可以使用绝对路径,甚至还可以使用自已的fork仓库。
3.3. 使用fork仓库
有时在使用开源的依赖包时发现了bug,在开源版本还未修改或者没有新的版本发布时,你可以使用fork仓库,在fork仓库中进行bug fix。 你可以在fork仓库上发布新的版本,并相应的修改go.mod
来使用fork仓库。
比如,我fork了开源包github.com/google/uuid
,fork仓库地址为github.com/RainbowMango/uuid
,那我们就可以在fork仓库里修改bug并发布新的版本v1.1.2
,此时使用fork仓库的项目中go.mod
中replace部分可以相应的做如下修改:
github.com/google/uuid v1.1.1 => github.com/RainbowMango/uuid v1.1.2
需要说明的是,使用fork仓库仅仅是临时的做法,一旦开源版本变得可用,需要尽快切换到开源版本。
3.4 禁止被依赖
另一种使用replace
的场景是你的module不希望被直接引用,比如开源软件kubernetes,在它的go.mod
中require
部分有大量的v0.0.0
依赖,比如:
module k8s.io/kubernetes
require (
...
k8s.io/api v0.0.0
k8s.io/apiextensions-apiserver v0.0.0
k8s.io/apimachinery v0.0.0
k8s.io/apiserver v0.0.0
k8s.io/cli-runtime v0.0.0
k8s.io/client-go v0.0.0
k8s.io/cloud-provider v0.0.0
...
)
由于上面的依赖都不存在v0.0.0版本,所以其他项目直接依赖k8s.io/kubernetes
时会因无法找到版本而无法使用。 因为Kubernetes不希望作为module被直接使用,其他项目可以使用kubernetes其他子组件。
kubernetes 对外隐藏了依赖版本号,其真实的依赖通过
replace
指定:
replace (
k8s.io/api => ./staging/src/k8s.io/api
k8s.io/apiextensions-apiserver => ./staging/src/k8s.io/apiextensions-apiserver
k8s.io/apimachinery => ./staging/src/k8s.io/apimachinery
k8s.io/apiserver => ./staging/src/k8s.io/apiserver
k8s.io/cli-runtime => ./staging/src/k8s.io/cli-runtime
k8s.io/client-go => ./staging/src/k8s.io/client-go
k8s.io/cloud-provider => ./staging/src/k8s.io/cloud-provider
)
前面我们说过,replace
指令在当前模块不是main module
时会被自动忽略的,Kubernetes正是利用了这一特性来实现对外隐藏依赖版本号来实现禁止直接引用的目的。
3.5 引入本地包(作用同3.2)
如果想在你的模块如果想引入你本地其他地方的模块,可以尝试通过 replace 指定目录,而且前提是你的 article 也得 go 的一个模块,而不是按 gopath 下的某个包来引入。
我简答举个例子吧,比如现在有两个项目,分别是 blog 和 article,结果如下:
├─article
│ article.go
│ go.mod
│
├─blog
│ go.mod
│ main.go
blog 是应用的入口,main 所在位置,而 article 可以理解为你写的一个公共的库,其中提供了一个函数 Hello()。现在,要在 blog 中调用 article 中的 Hello() 函数。
article 模块中的 go.mod 内容如下:
module article
go 1.13
article.go 内容如下:
package article
func Hello() string {
return "Hello"
}
blog 模块中的 go.mod 内容如下:
go 1.13
require github.com/article v0.0.0-incompatible // 引入这个包
replace github.com/article => ../article // 此处作用:将此包指向本地目录的路径
此处的 replace 稍微介绍下,之所以要是 github.com/article 的格式,是因为在 go1.13 中, go module 名称规范要求路径的第一部分必须满足域名规范,否则可能汇报类似 malformed module path "article": missing dot in first path element
这样的错误。当然,在 go1.12 不会有报这个错误。建议的话,如果是公司内部使用,可以替换成公司内部域名。
replace 的第二个参数指定了不从远程获取,而是本地某个路径下的模块替换 github.com/article。
main.go 的内容如下:
package main
import (
"fmt"
"github.com/article"
)
func main() {
fmt.Println("Hello")
fmt.Println(article.Hello())
}
此时,在 blog 执行 go run main.go 是可以成功运行的。
四、解决 k8s 依赖问题(k8s.io/kubernetes拉取不到)
问题
$ go get k8s.io/kubernetes
server response: not found: k8s.io/api@v0.0.0
- 解决方法 https://suraj.io/post/2021/05/k8s-import/
# 脚本
$ vi download-deps.sh
#!/bin/bash
VERSION=${1#"v"}
if [ -z "$VERSION" ]; then
echo "Please specify the Kubernetes version: e.g."
echo "./download-deps.sh v1.21.0"
exit 1
fi
set -euo pipefail
# Find out all the replaced imports, make a list of them.
MODS=($(
curl -sS "https://raw.githubusercontent.com/kubernetes/kubernetes/v${VERSION}/go.mod" |
sed -n 's|.*k8s.io/\(.*\) => ./staging/src/k8s.io/.*|k8s.io/\1|p'
))
# Now add those similar replace statements in the local go.mod file, but first find the version that
# the Kubernetes is using for them.
for MOD in "${MODS[@]}"; do
V=$(
go mod download -json "${MOD}@kubernetes-${VERSION}" |
sed -n 's|.*"Version": "\(.*\)".*|\1|p'
)
go mod edit "-replace=${MOD}=${MOD}@${V}"
done
go get "k8s.io/kubernetes@v${VERSION}"
go mod download
# 执行
$ chmod u+x download-deps.sh
$ ./download-deps.sh v1.21.0 # 可替换为相应的版本
解决方法
# 删除原有错误 mod 建立新 mod 文件
$ go mod init github.com/oceanweave/admission-webhook-sample
# 执行上述脚本 注意要与实际 k8s 环境对应
$ ./download-deps.sh v1.21.1 # 可替换为相应的版本
# 执行完成后 在 go mod 中添加如下内容
go 1.17 # 注意所需 go 版本 可更改
require (
github.com/golang/glog v1.0.0
k8s.io/api v0.21.1
k8s.io/apimachinery v0.21.1 # 更改对应版本
k8s.io/client-go v1.5.2
k8s.io/kubernetes v1.21.1 # 注意开头是 1
)
# 之后整理依赖
$ go mod tidy
结果
module github.com/oceanweave/admission-webhook-sample
go 1.17
require (
k8s.io/api v0.21.1
k8s.io/apimachinery v0.21.1
k8s.io/client-go v1.5.2
k8s.io/klog v1.0.0
)
replace k8s.io/api => k8s.io/api v0.21.1
replace k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.21.1
replace k8s.io/apimachinery => k8s.io/apimachinery v0.21.2-rc.0
replace k8s.io/apiserver => k8s.io/apiserver v0.21.1
replace k8s.io/cli-runtime => k8s.io/cli-runtime v0.21.1
replace k8s.io/client-go => k8s.io/client-go v0.21.1
replace k8s.io/cloud-provider => k8s.io/cloud-provider v0.21.1
replace k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.21.1
replace k8s.io/code-generator => k8s.io/code-generator v0.21.2-rc.0
replace k8s.io/component-base => k8s.io/component-base v0.21.1
replace k8s.io/component-helpers => k8s.io/component-helpers v0.21.1
replace k8s.io/controller-manager => k8s.io/controller-manager v0.21.1
replace k8s.io/cri-api => k8s.io/cri-api v0.21.2-rc.0
replace k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.21.1
replace k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.21.1
replace k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.21.1
replace k8s.io/kube-proxy => k8s.io/kube-proxy v0.21.1
replace k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.21.1
replace k8s.io/kubectl => k8s.io/kubectl v0.21.1
replace k8s.io/kubelet => k8s.io/kubelet v0.21.1
replace k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.21.1
replace k8s.io/metrics => k8s.io/metrics v0.21.1
replace k8s.io/mount-utils => k8s.io/mount-utils v0.21.5-rc.0
replace k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.21.1
replace k8s.io/sample-cli-plugin => k8s.io/sample-cli-plugin v0.21.1
replace k8s.io/sample-controller => k8s.io/sample-controller v0.21.1
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v0.4.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.4.3 // indirect
github.com/google/go-cmp v0.5.2 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/googleapis/gnostic v0.4.1 // indirect
github.com/imdario/mergo v0.3.5 // indirect
github.com/json-iterator/go v1.1.10 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/net v0.0.0-20210224082022-3d97a244fca7 // indirect
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073 // indirect
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect
golang.org/x/text v0.3.4 // indirect
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
google.golang.org/appengine v1.6.5 // indirect
google.golang.org/protobuf v1.25.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/klog/v2 v2.8.0 // indirect
k8s.io/utils v0.0.0-20201110183641-67b214c5f920 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.1.0 // indirect
sigs.k8s.io/yaml v1.2.0 // indirect
)