标签 RussCox 下的文章

Go module机制下升级major版本号的实践

Go module机制Go 1.11版本引入,虽然也伴随着不小的质疑声,但总体上Go社区多数Gopher是接受go module的,很多标杆式的Go项目(比如kubernetes、kubernetes client-go等)也都逐渐转向了Go module,并且Gopher也在向core team反馈了自己的建议和问题。Go core team也在go module最初设计的基础上持续进行着改进,比如:即将到来的Go 1.13版本中将增加默认GOPROXY(https://proxy.golang.org)、GOSUMDB(sum.golang.org);增加GONOPROXY、GONOSUMDB以应对私有module的处理;不断丰富的go mod子命令功能等。

随着Go module应用的日益逐步广泛和深入,Gopher们也开始遇到一些最初使用Go module时未曾遇到过的问题,比如升级major版本号(这是由于多数Go project仍处于untag的状态或者1.x.x状态,因此在go module引入初期,少有gopher遇到该类问题)。这篇文章我们就来简单看看如何在go module机制下面升级库的主版本号(major version number)。

一. Go module的“semantic import versioning”

在Russ Cox关于go module的系列论述文章“semantic import versioning”一文中,Russ说明了Go import包兼容性的总原则:

如果新旧版本的包使用相同的导入路径(import path),那么新包与旧包是兼容的。

也就是说如果新旧两个包不兼容,那么应该采用不同的导入路径。

因此,Russ采用了将“major版本”作为导入路径的一部分的设计。这种设计支持在同一个项目或go source文件中import同一个module下的package的不同版本。同一个package虽然包名字相同,但是import path不同。vN作为import path的一部分将用于区分包的不同版本。同时在同一个源文件中,我们可以使用包别名的方式来区分同一个包的不同版本,比如:

import (

    "github.com/bigwhite/foo/bar"

    barV2 "github.com/bigwhite/foo/v2/bar"

    ... ...

)

go module的这种设计对Go包的consumer(包的使用者)来说似乎并未有太多额外工作,但是这给Go包的author们带来了一定的复杂性,他们需要考虑在go module机制下如何将自己的Go module升级major version。稍有不慎,可能就会导致自身代码库的混乱或者package consumer侧无法通过编译或执行行为的混乱。

下面我们就从go package author的角度实践一下究竟该如何做module major版本号的升级。Go module为go package author提供了两种major version升级的方案,我们下面逐一看一下。我们的实验环境基于go 1.12.5 (ubuntu 16.04)。

二. 使用“major branch”方案

“major branch”方案对于多数gopher来说是一个过渡比较自然的方案,它通过建立vN分支并基于vN分支打vN.x.x的tag的方式做major version的升级。当然是否建立vN分支以及打vN.x.x tag都是一个可选的操作。

我们在bitbucket.org上建立一个公共仓库:bitbucket.org/bigwhite/modules-major-branch,其初始结构和代码如下(注意:此时本地开发环境中GO111MODULE=on):

# tree -LF 2 modules-major-branch
modules-major-branch
├── foo/
│   └── foo.go
├── go.mod
└── README.md

1 directory, 3 files

//go.mod

# cat go.mod
module bitbucket.org/bigwhite/modules-major-branch

go 1.12

// foo.go

package foo

import "fmt"

func Foo() {
        fmt.Println("foo.Foo of module: bitbucket.org/bigwhite/modules-major-branch pre-v1")
}

接下来,我们建立modules-major-branch/foo包的消费者项目:modules-major-branch-test

# tree -LF 1 ./modules-major-branch-test/
./modules-major-branch-test/
├── go.mod
├── go.sum
└── main.go

0 directories, 3 files

# cat go.mod
module bitbucket.org/bigwhite/modules-major-branch-test

go 1.12

# cat main.go
package main

import (
    "bitbucket.org/bigwhite/modules-major-branch/foo"
)

func main() {
    foo.Foo()
}

我们run一下“消费者”:

# go run main.go
go: finding bitbucket.org/bigwhite/modules-major-branch/foo latest
go: finding bitbucket.org/bigwhite/modules-major-branch latest
go: downloading bitbucket.org/bigwhite/modules-major-branch v0.0.0-20190602132049-2d924da2e295
go: extracting bitbucket.org/bigwhite/modules-major-branch v0.0.0-20190602132049-2d924da2e295
foo.Foo of module: bitbucket.org/bigwhite/modules-major-branch pre-v1

我们看到在这个阶段消费成功。

作为modules-major-branch的author,随着module功能演进,modules-major-branch到达了发布1.0版本的节点:

# cat foo/foo.go
package foo

import "fmt"

func Foo() {
    fmt.Println("foo.Foo of module: bitbucket.org/bigwhite/modules-major-branch v1.0.0")
}

# git tag v1.0.0
# git push --tag origin master

接下来,我们让consumer升级对modules-major-branch/foo的依赖到v1.0.0。这种升级是不会自动进行,是需要consumer的开发者自己决策后手工进行的,否则会给开发者带来困惑。我们通过go mod edit命令修改consumer的require:

# go mod edit -require=bitbucket.org/bigwhite/modules-major-branch@v1.0.0

# cat go.mod
module bitbucket.org/bigwhite/modules-major-branch-test

go 1.12

require bitbucket.org/bigwhite/modules-major-branch v1.0.0

我们来运行一下升级依赖后的程序:

# go run main.go
go: finding bitbucket.org/bigwhite/modules-major-branch v1.0.0
go: downloading bitbucket.org/bigwhite/modules-major-branch v1.0.0
go: extracting bitbucket.org/bigwhite/modules-major-branch v1.0.0
foo.Foo of module: bitbucket.org/bigwhite/modules-major-branch v1.0.0

从pre-v1到v1在最新的go module机制中还算不上major版本的升级,接下来我们就来看看foo包的作者应该如何对modules-major-branch module做出不兼容的升级:v1 -> v2。

当modules-major-branch module即将做出不兼容升级时,一般会为当前版本建立维护分支(比如:v1分支,并在v1分支上继续对v1版本进行维护、打补丁),然后再在master分支上做出不兼容的修改。

# git checkout -b v1

# git checkout master

# cat foo/foo.go
package foo

import "fmt"

func Foo2() {
    fmt.Println("foo.Foo2 of module: bitbucket.org/bigwhite/modules-major-branch v2.0.0")
}

从代码可以看到,在master分支上,我们删除了foo包中的Foo函数,新增了Foo2函数。但仅做这些还不够。在本文一开始我们就提到过原则:如果新旧两个包不兼容,那么应该采用不同的导入路径。我们为modules-major-branch module做出了不兼容的修改,也需要modules-major-branch module有着不同的导入路径,我们需要修改modules-major-branch module的module根路径:

# cat go.mod
module bitbucket.org/bigwhite/modules-major-branch/v2

go 1.12

# git tag v2.0.0
# git push --tag origin master

我们在module根路径后面加上了v2,并基于master建立了tag: v2.0.0。

我们再来看看consumer端应该如何应对modules-major-branch module的不兼容修改。如果consumer要使用最新的Foo2函数的话,我们需要对main.go做出如下改动:

//modules-major-branch-test/main.go

package main

import (
    "bitbucket.org/bigwhite/modules-major-branch/v2/foo"
)

func main() {
    foo.Foo2()
}

接下来我们不需要手工修改modules-major-branch-test的go.mod中依赖,直接运行go run即可:

# go run main.go
go: finding bitbucket.org/bigwhite/modules-major-branch/v2/foo latest
go: finding bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0
go: downloading bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0
go: extracting bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0
foo.Foo2 of module: bitbucket.org/bigwhite/modules-major-branch v2.0.0

我们看到go编译器会自动发现依赖变更,并下载对应的包并更新go.mod和go.num:

# cat go.mod
module bitbucket.org/bigwhite/modules-major-branch-test

go 1.12

require (
    bitbucket.org/bigwhite/modules-major-branch v1.0.0
    bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0 // indirect
)

modules-major-branch-test此时已经不再需要依赖v1.0.0了,我们可以通过go mod tidy清理一下go.mod中的依赖:

# go mod tidy
# cat go.mod
module bitbucket.org/bigwhite/modules-major-branch-test

go 1.12

require bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0

我们看到:现在就只剩下对modules-major-branch v2的依赖了。

后续modules-major-branch可以在master分支上持续演进,直到又有不兼容改动时,可以基于master建立v2维护分支,master分支将升级为v3(module)。

再小结一下:

对包的作者而言,升级major版本号需要:

  • 升级module的根路径,增加vN

  • 建立vN.x.x形式的tag(可选,如果不打tag,go会在consumer的go.mod中使用伪版本号,比如:bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0-20190603050009-28a5b8da279e)

如果modules-major-branch内部有相互的包引用,那么在升级major号的时候,这些包的import路径也要增加vN,否则就会存在在高major version的代码中引用低major version包代码的情况,这也是包作者最容易忽略的事情。github.com/marwan-at-work/mod是一个为module作者提供的升级/降级major version号的工具,它可以帮助包作者方便地自动修改项目内所有源文件中的import path。有gopher已经提出希望go官方提供upgrade/downgrade的支持,但目前core team尚未明确是否增加。

对于consumer而言,升级依赖包的major版本号,只需要在import包时在import path中增加vN即可,当然代码中也要针对不兼容的部分进行修改,然后go工具会自动下载相关包。

三. 使用 “major subdirectory”方案

go module机制还提供了一种我个人觉得较为怪异的方案或者说用起来不那么自然的方案,那就是利用子目录分割不同主版本。如果某个module目前已经演化到v3版本了,那么这个module所在仓库的目录结构应该是这样的:

# tree modules-major-subdir
modules-major-subdir
├── bar
│   └── bar.go
├── go.mod
├── v2
│   ├── bar
│   │   └── bar.go
│   └── go.mod
└── v3
    ├── bar
    │   └── bar.go
    └── go.mod

这里直接用vN作为子目录名字,在代码仓库中将不同版本module放置在不同的subdir中,这样即便不打tag,通过subdir也能找到对应版本的module包。以上图中的v2为例,该子目录下go.mod如下:

# cat go.mod
module bitbucket.org/bigwhite/modules-major-subdir/v2

go 1.12

v3也是类似。在各自子目录中,module的根路径都是带有vN扩展的。

接下来,我们就来创建consumer来分别调用不同版本的modules-major-subdir/bar包。和modules-major-branch-test类似,我们建立modules-major-subdir-test来作为consumer调用modules-major-subdir/bar包:

// modules-major-subdir-test

# cat go.mod
module bitbucket.org/bigwhite/modules-major-subdir-test

go 1.12

# cat main.go
package main

import (
    "bitbucket.org/bigwhite/modules-major-subdir/bar"
)

func main() {
    bar.Bar()
}

运行一下consumer:

# go run main.go

go: finding bitbucket.org/bigwhite/modules-major-subdir/bar latest
go: finding bitbucket.org/bigwhite/modules-major-subdir latest
go: downloading bitbucket.org/bigwhite/modules-major-subdir v0.0.0-20190603053114-50b15f581aba
go: extracting bitbucket.org/bigwhite/modules-major-subdir v0.0.0-20190603053114-50b15f581aba
bar.Bar of module: bitbucket.org/bigwhite/modules-major-subdir

我们修改main.go,调用v2版本bar包中Bar2函数:

package main

import (
    "bitbucket.org/bigwhite/modules-major-subdir/v2/bar"
)

func main() {
    bar.Bar2()
}

再次运行main.go:

# go run main.go
go: finding bitbucket.org/bigwhite/modules-major-subdir v0.0.0-20190603053114-50b15f581aba
go: downloading bitbucket.org/bigwhite/modules-major-subdir v0.0.0-20190603053114-50b15f581aba
go: extracting bitbucket.org/bigwhite/modules-major-subdir v0.0.0-20190603053114-50b15f581aba
go: finding bitbucket.org/bigwhite/modules-major-subdir/v2/bar latest
go: finding bitbucket.org/bigwhite/modules-major-subdir/v2 latest
go: downloading bitbucket.org/bigwhite/modules-major-subdir/v2 v2.0.0-20190603063223-4be5d54167e9
go: extracting bitbucket.org/bigwhite/modules-major-subdir/v2 v2.0.0-20190603063223-4be5d54167e9
bar.Bar2 of module: bitbucket.org/bigwhite/modules-major-subdir v2

我们看到:go编译器自动找到了位于modules-major-subdir仓库下v2子目录下的v2版本bar包。

从demo来看,似乎这种通过subdir方式来实现major version升级的方式更为“简单”一些。但笔者总感觉这种方式有些“怪”,尤其是在与tag交叉使用时可能会带来一些困惑,其他主流语言也鲜有使用这种方式进行major version升级的。另外一旦使用这种方式,似乎也很难利用git工具在不同major版本之间进行代码的merge(复用)了。

另外和major branch方案一样,如果module内部有相互的包引用,那么在升级major号的时候,这些包的import路径也要增加vN,否则也会存在在高major version的代码中引用低major version包代码的情况。

四. 小结

Go module作为主流语言依赖管理思路之外的一个“探索性”创新,势必在初期要有一段坎坷的道路要走。好事多磨,相信经过Go 1.11~Go 1.13三个版本的改进以及社区在工具方面对go module的逐渐的完善的支持,Go module会成为gopher日常Go开发的一柄利器,彻底解决Go的包依赖问题。

上述demo源码可在bitbucket.org/bigwhite下找到。

另外这里要提一点:国内的码云(gitee.com)目前对go module major version升级支持的还有问题。同样的操作,但在gitee.com下总是提示:“go.mod has post-v0 module path”


我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

我的联系方式:

微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

初窥Go module

自2007年“三巨头(Robert Griesemer, Rob Pike, Ken Thompson)”提出设计和实现Go语言以来,Go语言已经发展和演化了十余年了。这十余年来,Go取得了巨大的成就,先后在2009年和2016年当选TIOBE年度最佳编程语言,并在全世界范围内拥有数量庞大的拥趸。不过和其他主流编程语言一样,Go语言也不是完美的,不能满足所有开发者的“口味”。这些年来Go在“包依赖管理”和“缺少泛型”两个方面饱受诟病,它们也是Go粉们最希望Go核心Team重点完善的两个方面。

今年(2018)年初,Go核心Team的技术leader,也是Go Team最早期成员之一的Russ Cox个人博客上连续发表了七篇文章,系统阐述了Go team解决“包依赖管理”的技术方案: vgo。vgo的主要思路包括:Semantic Import VersioningMinimal Version Selection引入Go module等。这七篇文章的发布引发了Go社区激烈地争论,尤其是MVS(最小版本选择)与目前主流的依赖版本选择方法的相悖让很多传统Go包管理工具的维护者“不满”,尤其是“准官方工具”:dep。vgo方案的提出也意味着dep项目的生命周期即将进入尾声。

5月份,Russ Cox的Proposal “cmd/go: add package version support to Go toolchain”被accepted,这周五早些时候Russ Cox将vgo的代码merge到Go主干,并将这套机制正式命名为“go module”。由于vgo项目本身就是一个实验原型,merge到主干后,vgo这个术语以及vgo项目的使命也就就此结束了。后续Go modules机制将直接在Go主干上继续演化。

Go modules是go team在解决包依赖管理方面的一次勇敢尝试,无论如何,对Go语言来说都是一个好事。在本篇文章中,我们就一起来看看这个新引入的go modules机制。

一. 建立试验环境

由于加入go modules experiment机制的Go 1.11版本尚未正式发布,且go 1.11 beta1版本发布在go modules merge到主干之前,因此我们要进行go module试验只能使用Go tip版本,即主干上的最新版本。我们需要通过编译Go源码包的方式获得支持go module的go编译器:

编译Go项目源码的前提是你已经安装了一个发布版,比如Go 1.10.3。然后按照下面步骤执行即可:

$ git clone https://github.com/golang/go.git
$ mv go go-tip
$ cd go-tip
$ ./all.bash
Building Go cmd/dist using /root/.bin/go1.10.2.
Building Go toolchain1 using /root/.bin/go1.10.2.
Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.
Building Go toolchain2 using go_bootstrap and Go toolchain1.
Building Go toolchain3 using go_bootstrap and Go toolchain2.
Building packages and commands for linux/amd64.
##### Testing packages.
ok      archive/tar    0.026s
... ...
##### API check

ALL TESTS PASSED
---
Installed Go for linux/amd64 in /root/.bin/go-tip
Installed commands in /root/.bin/go-tip/bin
*** You need to add /root/.bin/go-tip/bin to your PATH.

验证源码编译方式的安装结果:

# ./go version
go version devel +a241922 Fri Jul 13 00:03:31 2018 +0000 linux/amd64

查看有关go module的手册:

$  ./go help mod
usage: go mod [-v] [maintenance flags]

Mod performs module maintenance operations as specified by the
following flags, which may be combined.

The -v flag enables additional output about operations performed.

The first group of operations provide low-level editing operations
for manipulating go.mod from the command line or in scripts or
other tools. They read only go.mod itself; they do not look up any
information about the modules involved.

The -init flag initializes and writes a new go.mod to the current directory,
in effect creating a new module rooted at the current directory.
The file go.mod must not already exist.
If possible, mod will guess the module path from import comments
(see 'go help importpath') or from version control configuration.
To override this guess, use the -module flag.
(Without -init, mod applies to the current module.)

The -module flag changes (or, with -init, sets) the module's path
(the go.mod file's module line).
... ...

无法通过编译源码的方式获取go tip版的小伙伴们也不用着急,在后续即将发布的go 1.11 beta2版本中将会包含对go modules的支持,到时候按常规方式安装beta2即可体验go modules。

二. 传统Go构建以及包依赖管理的回顾

Go在构建设计方面深受Google内部开发实践的影响,比如go get的设计就深受Google内部单一代码仓库(single monorepo)和基于主干(trunk/mainline based)的开发模型的影响:只获取Trunk/mainline代码和版本无感知。

img{512x368}

Google内部基于主干的开发模型:
– 所有开发人员基于主干trunk/mainline开发:提交到trunk或从trunk获取最新的代码(同步到本地workspace)
– 版本发布时,建立Release branch,release branch实质上就是某一个时刻主干代码的快照;
– 必须同步到release branch上的bug fix和增强改进代码也通常是先在主干上提交(commit),然后再cherry-pick到release branch上

我们知道go get获取的代码会放在$GOPATH/src下面,而go build会在$GOROOT/src和$GOPATH/src下面按照import path去搜索package,由于go get 获取的都是各个package repo的trunk/mainline的代码,因此,Go 1.5之前的Go compiler都是基于目标Go程序依赖包的trunk/mainline代码去编译的。这样的机制带来的问题是显而易见的,至少包括:

  • 因依赖包的trunk的变化,导致不同人获取和编译你的包/程序时得到的结果实质是不同的,即不能实现reproduceable build
  • 因依赖包的trunk的变化,引入不兼容的实现,导致你的包/程序无法通过编译
  • 因依赖包演进而无法通过编译,导致你的包/程序无法通过编译

为了实现reporduceable build,Go 1.5引入了Vendor机制,Go编译器会优先在vendor下搜索依赖的第三方包,这样如果开发者将特定版本的依赖包存放在vendor下面并提交到code repo,那么所有人理论上都会得到同样的编译结果,从而实现reporduceable build。

在Go 1.5发布后的若干年,gopher们把注意力都集中在如何利用vendor解决包依赖问题,从手工添加依赖到vendor、手工更新依赖,到一众包依赖管理工具的诞生:比如: govendorglide以及号称准官方工具的dep,努力地尝试着按照当今主流思路解决着诸如:“钻石型依赖”等难题。

正当gopher认为dep将“顺理成章”地升级为go toolchain一部分的时候,vgo横空出世,并通过对“Semantic Import Versioning”和”Minimal Version Selected”的设定,在原Go tools上简单快速地实现了Go原生的包依赖管理方案 。vgo就是go module的前身。

三. go modules定义、experiment开关以及“依赖管理”的工作模式

通常我们会在一个repo(仓库)中创建一组Go package,repo的路径比如:github.com/bigwhite/gocmpp会作为go package的导入路径(import path),Go 1.11给这样的一组在同一repo下面的packages赋予了一个新的抽象概念: module,并启用一个新的文件go.mod记录module的元信息。

不过一个repo对应一个module这种说法其实并不精确也并不正确,一个repo当然可以拥有多个module,很多公司或组织是喜欢用monorepo的,这样势必有在单一的monorepo建立多个module的需求,显然go modules也是支持这种情况的。

img{512x368}
图:single repo,single module

img{512x368}
图:single monorepo,multiple modules

是时候上代码了!

我们在~/test下建立hello目录(注意:$GOPATH=~/go,显然hello目录并不在GOPATH下面)。hello.go的代码如下:

// hello.go
package main

import "bitbucket.org/bigwhite/c"

func main() {
    c.CallC()
}

我们构建一下hello.go这个源码文件:

# go build hello.go
hello.go:3:8: cannot find package "bitbucket.org/bigwhite/c" in any of:
    /root/.bin/go-tip/src/bitbucket.org/bigwhite/c (from $GOROOT)
    /root/go/src/bitbucket.org/bigwhite/c (from $GOPATH)

构建错误!错误原因很明了:在本地的GOPATH下并没有找到bitbucket.org/bigwhite/c路径的package c。传统fix这个问题的方法是手工将package c通过go get下载到本地(并且go get会自动下载package c所依赖的package d):

# go get bitbucket.org/bigwhite/c
# go run hello.go
call C: master branch
   --> call D:
    call D: master branch
   --> call D end

这种我们最熟悉的Go compiler从$GOPATH下(以及vendor目录下)搜索目标程序的依赖包的模式称为:“GOPATH mode”

GOPATH是Go最初设计的产物,在Go语言快速发展的今天,人们日益发现GOPATH似乎不那么重要了,尤其是在引入vendor以及诸多包管理工具后。并且GOPATH的设置还会让Go语言新手感到些许困惑,提高了入门的门槛。Go core team也一直在寻求“去GOPATH”的方案,当然这一过程是循序渐进的。Go 1.8版本中,如果开发者没有显式设置GOPATH,Go会赋予GOPATH一个默认值(在linux上为$HOME/go)。虽说不用再设置GOPATH,但GOPATH还是事实存在的,它在go toolchain中依旧发挥着至关重要的作用。

Go module的引入在Go 1.8版本上更进了一步,它引入了一种新的依赖管理mode:“module-aware mode”。在该mode下,某源码树(通常是一个repo)的顶层目录下会放置一个go.mod文件,每个go.mod文件定义了一个module,而放置go.mod文件的目录被称为module root目录(通常对应一个repo的root目录,但不是必须的)。module root目录以及其子目录下的所有Go package均归属于该module,除了那些自身包含go.mod文件的子目录。

在“module-aware mode”下,go编译器将不再在GOPATH下面以及vendor下面搜索目标程序依赖的第三方Go packages。我们来看一下在“module-aware mode”下hello.go的构建过程:

我们首先在~/test/hello下创建go.mod:

// go.mod
module hello

然后构建hello.go

# go build hello.go
go: finding bitbucket.org/bigwhite/d v0.0.0-20180714005150-3e3f9af80a02
go: finding bitbucket.org/bigwhite/c v0.0.0-20180714063616-861b08fcd24b
go: downloading bitbucket.org/bigwhite/c v0.0.0-20180714063616-861b08fcd24b
go: downloading bitbucket.org/bigwhite/d v0.0.0-20180714005150-3e3f9af80a02

# ./hello
call C: master branch
   --> call D:
    call D: master branch
   --> call D end

我们看到go compiler并没有去使用之前已经下载到GOPATH下的bitbucket.org/bigwhite/c和bitbucket.org/bigwhite/d,而是主动下载了这两个包并成功编译。我们看看执行go build后go.mod文件的内容:

# cat go.mod
module hello

require (
    bitbucket.org/bigwhite/c v0.0.0-20180714063616-861b08fcd24b
    bitbucket.org/bigwhite/d v0.0.0-20180714005150-3e3f9af80a02 // indirect
)

我们看到go compiler分析出了hello module的依赖,将其放入go.mod的require区域。由于c、d两个package均没有版本发布(打tag),因此go compiler使用了c、d的当前最新版,并以Pseudo-versions的形式记录之。并且我们看到:hello module并没有直接依赖d package,因此在d的记录后面通过注释形式标记了indirect,即非直接依赖,也就是传递依赖。

在“module-aware mode”下,go compiler将下载的依赖包缓存在$GOPATH/pkg/mod下面:

// $GOPATH/pkg/mod
# tree -L 3
.
├── bitbucket.org
│   └── bigwhite
│       ├── c@v0.0.0-20180714063616-861b08fcd24b
│       └── d@v0.0.0-20180714005150-3e3f9af80a02
├── cache
│   ├── download
│   │   ├── bitbucket.org
│   │   ├── golang.org
│   │   └── rsc.io
│   └── vcs
│       ├── 064503657de46d4574a6ab937a7a3b88fee03aec15729f7493a3dc8e35cc6d80
│       ├── 064503657de46d4574a6ab937a7a3b88fee03aec15729f7493a3dc8e35cc6d80.info
│       ├── 0c8659d2f971b567bc9bd6644073413a1534735b75ea8a6f1d4ee4121f78fa5b
... ...

我们看到c、d两个package也是按照“版本”进行缓存的,便于后续在“module-aware mode”下进行包构建使用。

Go modules机制在go 1.11中是experiment feature,按照Go的惯例,在新的experiment feature首次加入时,都会有一个特性开关,go modules也不例外,GO111MODULE这个临时的环境变量就是go module特性的experiment开关。GO111MODULE有三个值:auto、on和off,默认值为auto。GO111MODULE的值会直接影响Go compiler的“依赖管理”模式的选择(是GOPATH mode还是module-aware mode),我们详细来看一下:

  • 当GO111MODULE的值为off时,go modules experiment feature关闭,go compiler显然会始终使用GOPATH mode,即无论要构建的源码目录是否在GOPATH路径下,go compiler都会在传统的GOPATH和vendor目录(仅支持在gopath目录下的package)下搜索目标程序依赖的go package;

  • 当GO111MODULE的值为on时(export GO111MODULE=on),go modules experiment feature始终开启,与off相反,go compiler会始终使用module-aware mode,即无论要构建的源码目录是否在GOPATH路径下,go compiler都不会在传统的GOPATH和vendor目录下搜索目标程序依赖的go package,而是在go mod命令的缓存目录($GOPATH/pkg/mod)下搜索对应版本的依赖package;

  • 当GO111MODULE的值为auto时(不显式设置即为auto),也就是我们在上面的例子中所展现的那样:使用GOPATH mode还是module-aware mode,取决于要构建的源码目录所在位置以及是否包含go.mod文件。如果要构建的源码目录不在以GOPATH/src为根的目录体系下,且包含go.mod文件(两个条件缺一不可),那么使用module-aware mode;否则使用传统的GOPATH mode。

四. go modules的依赖版本选择

1. build list和main module

go.mod文件一旦创建后,它的内容将会被go toolchain全面掌控。go toolchain会在各类命令执行时,比如go get、go build、go mod等修改和维护go.mod文件。

之前的例子中,hello module依赖的c、d(indirect)两个包均没有显式的版本信息(比如: v1.x.x),因此go mod使用Pseudo-versions机制来生成和记录c, d的“版本”,我们可以通过下面命令查看到这些信息:

# go list -m -json all
{
    "Path": "hello",
    "Main": true,
    "Dir": "/root/test/hello"
}
{
    "Path": "bitbucket.org/bigwhite/c",
    "Version": "v0.0.0-20180714063616-861b08fcd24b",
    "Time": "2018-07-14T06:36:16Z",
    "Dir": "/root/go/pkg/mod/bitbucket.org/bigwhite/c@v0.0.0-20180714063616-861b08fcd24b"
}
{
    "Path": "bitbucket.org/bigwhite/d",
    "Version": "v0.0.0-20180714005150-3e3f9af80a02",
    "Time": "2018-07-14T00:51:50Z",
    "Indirect": true,
    "Dir": "/root/go/pkg/mod/bitbucket.org/bigwhite/d@v0.0.0-20180714005150-3e3f9af80a02"
}

go list -m输出的信息被称为build list,也就是构建当前module所要构建的所有相关package(及版本)的列表。在输出信息中我们看到 “Main”: true这一信息,标识当前的module为“main module”。所谓main module,即是go build命令执行时所在当前目录所归属的那个module,go命令会在当前目录、当前目录的父目录、父目录的父目录…等下面寻找go.mod文件,所找到的第一个go.mod文件对应的module即为main module。如果没有找到go.mod,go命令会提示下面错误信息:

# go build test/hello/hello.go
go: cannot find main module root; see 'go help modules'

当然我们也可以使用下面命令简略输出build list:

# go list -m all
hello
bitbucket.org/bigwhite/c v0.0.0-20180714063616-861b08fcd24b
bitbucket.org/bigwhite/d v0.0.0-20180714005150-3e3f9af80a02

2. module requirement

现在我们给c、d两个package打上版本信息:

package c:
v1.0.0
v1.1.0
v1.2.0

package d:
v1.0.0
v1.1.0
v1.2.0
v1.3.0

然后清除掉$GOPATH/pkg/mod目录,并将hello.mod重新置为初始状态(只包含module字段)。接下来,我们再来构建一次hello.go:

// ~/test/hello目录下

# go build hello.go
go: finding bitbucket.org/bigwhite/c v1.2.0
go: downloading bitbucket.org/bigwhite/c v1.2.0
go: finding bitbucket.org/bigwhite/d v1.3.0
go: downloading bitbucket.org/bigwhite/d v1.3.0

# ./hello
call C: v1.2.0
   --> call D:
    call D: v1.3.0
   --> call D end

# cat go.mod
module hello

require (
    bitbucket.org/bigwhite/c v1.2.0 // indirect (c package被标记为indirect,这似乎是当前版本的一个bug)
    bitbucket.org/bigwhite/d v1.3.0 // indirect
)

我们看到,再一次初始构建hello module时,Go compiler不再用最新的commit revision所对应的Pseudo-version,而是使用了c、d两个package的最新发布版(c:v1.2.0,d: v1.3.0)。

如果我们对使用的c、d版本有特殊约束,比如:我们使用package c的v1.0.0,package d的v1.1.0版本,我们可以通过go mod -require来操作go.mod文件,更新go.mod文件中的require段的信息:

# go mod -require=bitbucket.org/bigwhite/c@v1.0.0
# go mod -require=bitbucket.org/bigwhite/d@v1.1.0

# cat go.mod
module hello

require (
    bitbucket.org/bigwhite/c v1.0.0 // indirect
    bitbucket.org/bigwhite/d v1.1.0 // indirect
)

# go build hello.go
go: finding bitbucket.org/bigwhite/d v1.1.0
go: finding bitbucket.org/bigwhite/c v1.0.0
go: downloading bitbucket.org/bigwhite/c v1.0.0
go: downloading bitbucket.org/bigwhite/d v1.1.0

# ./hello
call C: v1.0.0
   --> call D:
    call D: v1.1.0
   --> call D end

我们看到由于我们显式地修改了对package c、d两个包的版本依赖约束,go build构建时会去下载package c的v1.0.0和package d的v1.1.0版本并完成构建。

3. module query

除了通过传入package@version给go mod -requirement来精确“指示”module依赖之外,go mod还支持query表达式,比如:

# go mod -require='bitbucket.org/bigwhite/c@>=v1.1.0'

go mod会对query表达式做求值,得出build list使用的package c的版本:

# cat go.mod
module hello

require (
    bitbucket.org/bigwhite/c v1.1.0
    bitbucket.org/bigwhite/d v1.1.0 // indirect
)

# go build hello.go
go: downloading bitbucket.org/bigwhite/c v1.1.0
# ./hello
call C: v1.1.0
   --> call D:
    call D: v1.1.0
   --> call D end

go mod对module query进行求值的算法是“选择最接近于比较目标的版本(tagged version)”。以上面例子为例:

query text: >=v1.1.0
比较的目标版本为v1.1.0
比较形式:>=

因此,满足这一query的最接近于比较目标的版本(tagged version)就是v1.1.0。

如果我们给package d增加一个约束“小于v1.3.0”,我们再来看看go mod的选择:

# go mod -require='bitbucket.org/bigwhite/d@<v1.3.0'
# cat go.mod
module hello

require (
    bitbucket.org/bigwhite/c v1.1.0 // indirect
    bitbucket.org/bigwhite/d <v1.3.0
)

# go build hello.go
go: finding bitbucket.org/bigwhite/d v1.2.0
go: downloading bitbucket.org/bigwhite/d v1.2.0

# ./hello
call C: v1.1.0
   --> call D:
    call D: v1.2.0
   --> call D end

我们看到go mod选择了package d的v1.2.0版本,根据module query的求值算法,v1.2.0恰是最接近于“小于v1.3.0”的tagged version。

用下面这幅示意图来呈现这一算法更为直观一些:

img{512x368}

4. minimal version selection(mvs)

到目前为止,我们所使用的example都是最最简单的,hello module所依赖的package c和package d并没有自己的go.mod,也没有定义自己的requirements。对于复杂的包依赖场景,Russ Cox在“Minimal Version Selection”一文中给过形象的算法解释(注意:这个算法仅是便于人类理解,但是性能低下,真正的实现并非按照这个算法实现):

img{512x368}
例子情景

img{512x368}
算法的形象解释

MVS以build list为中心,从一个空的build list集合开始,先加入main module(A1),然后递归计算main module的build list,我们看到在这个过程中,先得到C 1.2的build list,然后是B 1.2的build list,去重合并后形成A1的rough build list,选择集合中每个module的最新version,最终形成A1的build list。

我们改造一下我们的例子,让它变得复杂些!

首先,我们为package c添加go.mod文件,并为其打一个新版本:v1.3.0:

//bitbucket.org/bigwhite/c/go.mod
module bitbucket.org/bigwhite/c

require (
        bitbucket.org/bigwhite/d v1.2.0
)

在module bitbucket.org/bigwhite/c的module文件中,我们为其添加一个requirment: bitbucket.org/bigwhite/d@v1.2.0。

接下来,我们将hello module重置为初始状态,并删除$GOPATH/pkg/mod目录。我们修改一下hello module的hello.go如下:

package main

import "bitbucket.org/bigwhite/c"
import "bitbucket.org/bigwhite/d"

func main() {
    c.CallC()
    d.CallD()
}

我们让hello module也直接调用package d,并且我们在初始情况下,给hello module添加一个requirement:

module hello

require (
    bitbucket.org/bigwhite/d v1.3.0
)

好了,这次我们再来构建一下hello module:

# go build hello.go
go: finding bitbucket.org/bigwhite/d v1.3.0
go: downloading bitbucket.org/bigwhite/d v1.3.0
go: finding bitbucket.org/bigwhite/c v1.3.0
go: downloading bitbucket.org/bigwhite/c v1.3.0
go: finding bitbucket.org/bigwhite/d v1.2.0
# cat go.mod
module hello

require (
    bitbucket.org/bigwhite/c v1.3.0 // indirect
    bitbucket.org/bigwhite/d v1.3.0 // indirect
)

# ./hello
call C: v1.3.0
   --> call D:
    call D: v1.3.0
   --> call D end
call D: v1.3.0

我们看到经过mvs算法后,go compiler最终选择了d v1.3.0版本。这里也模仿Russ Cox的图解给出hello module的mvs解析示意图(不过我这个例子还是比较simple):

img{512x368}

5. 使用package d的v2版本

按照语义化版本规范,当出现不兼容性的变化时,需要升级版本中的major值,而go modules允许在import path中出现v2这样的带有major版本号的路径,表示所用的package为v2版本下的实现。我们甚至可以同时使用一个package的v0/v1和v2两个版本的实现。我们依旧使用上面的例子来实操一下如何在hello module中使用package d的两个版本的代码。

我们首先需要为package d建立module文件:go.mod,并标识出当前的module为:bitbucket.org/bigwhite/d/v2(为了保持与v0/v1各自独立演进,可通过branch的方式来实现),然后基于该版本打v2.0.0 tag。

// bitbucket.org/bigwhite/d
#cat go.mod
module bitbucket.org/bigwhite/d/v2

改造一下hello module,import d的v2版本:

// hello.go
package main

import "bitbucket.org/bigwhite/c"
import "bitbucket.org/bigwhite/d/v2"

func main() {
    c.CallC()
    d.CallD()
}

清理hello module的go.mod,仅保留对package c的requirement:

module hello

require (
    bitbucket.org/bigwhite/c v1.3.0
)

清理$GOPATH/pkg/mod目录,然后重新构建hello module:

# go build hello.go
go: finding bitbucket.org/bigwhite/c v1.3.0
go: finding bitbucket.org/bigwhite/d v1.2.0
go: downloading bitbucket.org/bigwhite/c v1.3.0
go: downloading bitbucket.org/bigwhite/d v1.2.0
go: finding bitbucket.org/bigwhite/d/v2 v2.0.0
go: downloading bitbucket.org/bigwhite/d/v2 v2.0.0

# cat go.mod
module hello

require (
    bitbucket.org/bigwhite/c v1.3.0 // indirect
    bitbucket.org/bigwhite/d/v2 v2.0.0 // indirect
)

# ./hello
call C: v1.3.0
   --> call D:
    call D: v1.2.0
   --> call D end
call D: v2.0.0

我们看到c package依然使用的是d的v1.2.0版本,而main中使用的package d已经是v2.0.0版本了。

五. go modules与vendor

在最初的设计中,Russ Cox是想彻底废除掉vendor的,但在社区的反馈下,vendor得以保留,这也是为了兼容Go 1.11之前的版本。

Go modules支持通过下面命令将某个module的所有依赖保存一份copy到root module dir的vendor下:

# go mod -vendor
# ls
go.mod    go.sum  hello.go  vendor/
# cd vendor
# ls
bitbucket.org/    modules.txt
# cat modules.txt
# bitbucket.org/bigwhite/c v1.3.0
bitbucket.org/bigwhite/c
# bitbucket.org/bigwhite/d v1.2.0
bitbucket.org/bigwhite/d
# bitbucket.org/bigwhite/d/v2 v2.0.0
bitbucket.org/bigwhite/d/v2

# tree .
.
├── bitbucket.org
│   └── bigwhite
│       ├── c
│       │   ├── c.go
│       │   ├── go.mod
│       │   └── README.md
│       └── d
│           ├── d.go
│           ├── README.md
│           └── v2
│               ├── d.go
│               ├── go.mod
│               └── README.md
└── modules.txt

5 directories, 9 files

这样即便在go modules的module-aware mode模式下,我们依然可以只用vendor下的package来构建hello module。比如:我们先删除掉$GOPATH/pkg/mod目录,然后执行:

# go build -getmode=vendor hello.go
# ./hello
call C: v1.3.0
   --> call D:
    call D: v1.2.0
   --> call D end
call D: v2.0.0

当然生成的vendor目录还可以兼容go 1.11之前的go compiler。不过由于go 1.11之前的go compiler不支持在GOPATH之外使用vendor机制,因此我们需要将hello目录copy到$GOPATH/src下面,再用go 1.10.2版本的compiler编译它:

# go version
go version go1.10.2 linux/amd64
~/test/hello# go build hello.go
hello.go:3:8: cannot find package "bitbucket.org/bigwhite/c" in any of:
    /root/.bin/go1.10.2/src/bitbucket.org/bigwhite/c (from $GOROOT)
    /root/go/src/bitbucket.org/bigwhite/c (from $GOPATH)
hello.go:4:8: cannot find package "bitbucket.org/bigwhite/d/v2" in any of:
    /root/.bin/go1.10.2/src/bitbucket.org/bigwhite/d/v2 (from $GOROOT)
    /root/go/src/bitbucket.org/bigwhite/d/v2 (from $GOPATH)

# cp -r hello ~/go/src
# cd ~/go/src/hello
# go build hello.go
# ./hello
call C: v1.3.0
   --> call D:
    call D: v1.2.0
   --> call D end
call D: v2.0.0

编译输出和程序的执行结果均符合预期。

六. 小结

go modules刚刚merge到go trunk中,问题还会有很多。merge后很多gopher也提出了诸多问题,可以在这里查到。当然哪位朋友如果也遇到了go modules的问题,也可以在go官方issue上提出来,帮助go team尽快更好地完善go 1.11的go modules机制。

go module的加入应该算是go 1.11版本最大的变化,go module的内容很多,短时间内我的理解也可能存在偏差和错误,欢迎广大gopher们交流指正。

参考资料:


51短信平台:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

我的联系方式:

微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats