【Go mod 学习之 replace 篇】解决 go 本地依赖、无法拉取依赖、禁止依赖等问题

参考

一、总览

go.mod文件中通过指令声明module信息,用于控制命令行工具进行版本选择。一共有四个指令可供使用:

  • module: 声明module名称;
  • require: 声明依赖以及其版本号;
  • replace: 替换require中声明的依赖,使用另外的依赖及其版本号;
  • exclude: 禁用指定的依赖;

其中modulerequire我们前面已介绍过,module用于指定module的名字,如module github.com/renhongcai/gomodule,那么其他项目引用该module时其import路径需要指定github.com/renhongcai/gomodulerequire用于指定依赖,如require github.com/google/uuid v1.1.1,该指令相当于告诉go build使用github.com/google/uuidv1.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.BritishEnglishlanguage.Greek分别打印数字1500,来查看不同语言对数字格式的处理,一个是1,500,另一个是1.500。此时就会分别引入"golang.org/x/text/language""golang.org/x/text/message"

执行go getgo 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.modrequire部分有大量的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
)
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值