息附加在包名上来说可能会造成一定的迷惑,但是这种做法可以最大限度的兼容现有代码,而golang则需要修改mod文件,修改引入路径,分散的修改往往导致潜在的缺陷,考虑到现有的golang生态这一做法显得不那么明智。同时将版本信息绑定进包名对于习惯了传统包管理器方案的用户(npm,pip)来说显得有些怪异,可能需要花上一些额外时间适应。
不过检验真理的标准永远都是实践,随着go1.12的发布我们最终会见分晓,对于go modules现在是给予耐心提出建议的阶段,评判还为时尚早。
replace的限制
go mod edit -replace
无疑是一个十分强大的命令,但强大的同时它的限制也非常多。
本部分你将看到两个例子,它们分别阐述了本地包替换的方法以及顶层依赖与间接依赖的区别,现在让我们进入第一个例子。
本地包替换
replace除了可以将远程的包进行替换外,还可以将本地存在的modules替换成任意指定的名字。
假设我们有如下的项目:
tree my-mod
my-mod
├── go.mod
├── main.go
└── pkg
├── go.mod
└── pkg.go
其中main.go负责调用my/example/pkg
中的Hello
函数打印一句“Hello”,my/example/pkg
显然是个不存在的包,我们将用本地目录的pkg
包替换它,这是main.go:
package main
import "my/example/pkg"
func main() {
pkg.Hello()
}
我们的pkg.go相对来说很简单:
package pkg
import "fmt"
func Hello() {
fmt.Println("Hello")
}
重点在于go.mod文件,虽然不推荐直接编辑mod文件,但在这个例子中与使用go mod edit
的效果几乎没有区别,所以你可以尝试自己动手修改my-mod/go.mod:
module my-mod
require my/example/pkg v0.0.0
replace my/example/pkg => ./pkg
至于pkg/go.mod,使用go mod init
生成后不用做任何修改,它只是让我们的pkg成为一个module,因为replace的源和目标都只能是go modules。
因为被replace的包首先需要被require(wiki说本地替换不用指定,然而我试了报错),所以在my-mod/go.mod中我们需要先指定依赖的包,即使它并不存在。对于一个会被replace的包,如果是用本地的module进行替换,那么可以指定版本为v0.0.0
(对于没有使用版本控制的包只能指定这个版本),否则应该和替换包的指定版本一致。
再看replace my/example/pkg => ./pkg
这句,与替换远程包时一样,只是将替换用的包名改为了本地module所在的绝对或相对路径。
一切准备就绪,我们运行go build
,然后项目目录会变成这样:
tree my-mod
my-mod
├── go.mod
├── main.go
├── my-mod
└── pkg
├── go.mod
└── pkg.go
那个叫my-mod的文件就是编译好的程序,我们运行它:
./my-mod
Hello
运行成功,my/example/pkg
已经替换成了本地的pkg
。
同时我们注意到,使用本地包进行替换时并不会生成go.sum所需的信息,所以go.sum文件也没有生成。
本地替换的价值在于它提供了一种使自动生成的代码进入go modules系统的途径,毕竟不管是go tools还是rpc工具,这些自动生成代码也是项目的一部分,如果不能纳入包管理器的管理范围想必会带来很大的麻烦。
顶层依赖与间接依赖
如果你因为golang.org/x/...
无法获取而使用replace进行替换,那么你肯定遇到过问题。明明已经replace的包为何还会去未替换的地址进行搜索和下载?
解释这个问题前先看一个go.mod的例子,这个项目使用的第三方模块使用了golang.org/x/...
的包,但项目中没有直接引用它们:
module schanclient
require (
github.com/PuerkitoBio/goquery v1.4.1
github.com/andybalholm/cascadia v1.0.0 // indirect
github.com/chromedp/chromedp v0.1.2
golang.org/x/net v0.0.0-20180824152047-4bcd98cce591 // indirect
)
注意github.com/andybalholm/cascadia v1.0.0
和golang.org/x/net v0.0.0-20180824152047-4bcd98cce591
后面的// indirect
,它表示这是一个间接依赖。
间接依赖是指在当前module中没有直接import,而被当前module使用的第三方module引入的包,相对的顶层依赖就是在当前module中被直接import的包。如果二者规则发生冲突,那么顶层依赖的规则覆盖间接依赖。
在这里golang.org/x/net
被github.com/chromedp/chromedp
引入,但当前项目未直接import,所以是一个间接依赖,而github.com/chromedp/chromedp
被直接引入和使用,所以它是一个顶层依赖。
而我们的replace命令只能管理顶层依赖,所以在这里你使用replace golang.org/x/net => github.com/golang/net
是没用的,这就是为什么会出现go build时仍然去下载golang.org/x/net
的原因。
那么如果我把// indirect
去掉了,那么不就变成顶层依赖了吗?答案当然是不行。不管是直接编辑还是go mod edit
修改,我们为go.mod添加的信息都只是对go mod
的一种提示而已,当运行go build
或是go mod tidy
时golang会自动更新go.mod导致某些修改无效,简单来说一个包是顶层依赖还是间接依赖,取决于它在本module中是否被直接import,而不是在go.mod文件中是否包含// indirect
注释。
限制
replace唯一的限制是它只能处理顶层依赖。
这样限制的原因也很好理解,因为对于包进行替换后,通常不能保证兼容性,对于一些使用了这个包的第三方module来说可能意味着潜在的缺陷,而允许顶层依赖的替换则意味着你对自己的项目有充足的自信不会因为replace引入问题,是可控的。相当符合golang的工程性原则。
也正如此replace的适用范围受到了相当的限制:
- 可以使用本地包替换将生成代码纳入go modules的管理
- 对于直接import的顶层依赖,可以替换不能正常访问的包或是过时的包
- go modules下import不再支持使用相对路径导入包,例如
import "./mypkg"
,所以需要考虑replace
除此之外的r