标签 TIOBE 下的文章

初窥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 1.9中值得关注的几个变化

Go语言在2016年当选tiobe index的年度编程语言。

img{512x368}

转眼间6个月过去了,Go在tiobe index排行榜上继续强势攀升,在最新公布的TIBOE INDEX 7月份的排行榜上,Go挺进Top10:

img{512x368}

还有不到一个月,Go 1.9版本也要正式Release了(计划8月份发布),当前Go 1.9的最新版本是go1.9beta2,本篇的实验环境也是基于该版本的,估计与final go 1.9版本不会有太大差异了。在今年的GopherChina大会上,我曾提到:Go已经演进到1.9,接下来是Go 1.10还是Go 2? 现在答案已经揭晓:Go 1.10。估计Go core team认为Go 1还有很多待改善和优化的地方,或者说Go2的大改时机依旧未到。Go team的tech lead Russ Cox将在今年的GopherCon大会上做一个题为”The Future of Go”的主题演讲,期待从Russ的口中能够得到一些关于Go未来的信息。

言归正传,我们还是来看看Go 1.9究竟有哪些值得我们关注的变化,虽然我个人觉得Go1.9的变动的幅度并不是很大^0^。

一、Type alias

Go 1.9依然属于Go1系,因此继续遵守Go1兼容性承诺。这一点在我的“值得关注的几个变化”系列文章中几乎每次都要提到。

不过Go 1.9在语言语法层面上新增了一个“颇具争议”的语法: Type Alias。关于type alias的proposal最初由Go语言之父之一的Robert Griesemer提出,并计划于Go 1.8加入Go语言。但由于Go 1.8的type alias实现过于匆忙,测试不够充分,在临近Go 1.8发布的时候发现了无法短时间解决的问题,因此Go team决定将type alias的实现从Go 1.8中回退

Go 1.9 dev cycle伊始,type alias就重新被纳入。这次Russ Cox亲自撰写文章《Codebase Refactoring (with help from Go)》为type alias的加入做铺垫,并开启新的discussion对之前Go 1.8的general alias语法形式做进一步优化,最终1.9仅仅选择了type alias,而不需要像Go 1.8中general alias那样引入新的操作符(=>)。这样,结合Go已实现的interchangeable constant、function、variable,外加type alias,Go终于在语言层面实现了对“Gradual code repair(渐进式代码重构)”理念的初步支持。

注:由于type alias的加入,在做Go 1.9相关的代码试验之前,最好先升级一下你本地编辑器/IDE插件(比如:vim-govscode-go)以及各种tools的版本。

官方对type alias的定义非常简单:

An alias declaration binds an identifier to the given type.

我们怎么来理解新增的type alias和传统的type definition的区别呢?

type T1 T2  // 传统的type defintion

vs.

type T1 = T2 //新增的type alias

把握住一点:传统的type definition创造了一个“新类型”,而type alias并没有创造出“新类型”。如果我们有一个名为“孙悟空”的类型,那么我们可以写出如下有意思的代码:

type  超级赛亚人  孙悟空
type  卡卡罗特 = 孙悟空

这时,我们拥有了两个类型:孙悟空超级赛亚人。我们以孙悟空这个类型为蓝本定义一个超级赛亚人类型;而当我们用到卡卡罗特这个alias时,实际用的就是孙悟空这个类型,因为卡卡罗特就是孙悟空,孙悟空就是卡卡罗特。

我们用几个小例子再来仔细对比一下:

1、赋值

Go强调“显式类型转换”,因此采用传统type definition定义的新类型在其变量被赋值时需对右侧变量进行显式转型,否则编译器就会报错。

//github.com/bigwhite/experiments/go19-examples/typealias/typedefinitions-assignment.go
package main

// type definitions
type MyInt int
type MyInt1 MyInt

func main() {
    var i int = 5
    var mi MyInt = 6
    var mi1 MyInt1 = 7

    mi = MyInt(i)  // ok
    mi1 = MyInt1(i) // ok
    mi1 = MyInt1(mi) // ok

    mi = i   //Error: cannot use i (type int) as type MyInt in assignment
    mi1 = i  //Error: cannot use i (type int) as type MyInt1 in assignment
    mi1 = mi //Error: cannot use mi (type MyInt) as type MyInt1 in assignment
}

而type alias并未创造新类型,只是源类型的“别名”,在类型信息上与源类型一致,因此可以直接赋值:

//github.com/bigwhite/experiments/go19-examples/typealias/typealias-assignment.go
package main

import "fmt"

// type alias
type MyInt = int
type MyInt1 = MyInt

func main() {
    var i int = 5
    var mi MyInt = 6
    var mi1 MyInt1 = 7

    mi = i // ok
    mi1 = i // ok
    mi1 = mi // ok

    fmt.Println(i, mi, mi1)
}

2、类型方法

Go1中通过type definition定义的新类型,新类型不会“继承”源类型的method set

// github.com/bigwhite/experiments/go19-examples/typealias/typedefinition-method.go
package main

// type definitions
type MyInt int
type MyInt1 MyInt

func (i *MyInt) Increase(a int) {
    *i = *i + MyInt(a)
}

func main() {
    var mi MyInt = 6
    var mi1 MyInt1 = 7
    mi.Increase(5)
    mi1.Increase(5) // Error: mi1.Increase undefined (type MyInt1 has no field or method Increase)
}

但是通过type alias方式得到的类型别名却拥有着源类型的method set(因为本就是一个类型),并且通过alias type定义的method也会反映到源类型当中:

// github.com/bigwhite/experiments/go19-examples/typealias/typealias-method1.go
package main

type Foo struct{}
type Bar = Foo

func (f *Foo) Method1() {
}

func (b *Bar) Method2() {
}

func main() {
    var b Bar
    b.Method1() // ok

    var f Foo
    f.Method2() // ok
}

同样对于源类型为非本地类型的,我们也无法通过type alias为其增加新method:

//github.com/bigwhite/experiments/go19-examples/typealias/typealias-method.go
package main

type MyInt = int

func (i *MyInt) Increase(a int) { // Error: cannot define new methods on non-local type int
    *i = *i + MyInt(a)
}

func main() {
    var mi MyInt = 6
    mi.Increase(5)
}

3、类型embedding

有了上面关于类型方法的结果,其实我们也可以直接知道在类型embedding中type definition和type alias的差异。

// github.com/bigwhite/experiments/go19-examples/typealias/typedefinition-embedding.go
package main

type Foo struct{}
type Bar Foo

type SuperFoo struct {
    Bar
}

func (f *Foo) Method1() {
}

func main() {
    var s SuperFoo
    s.Method1() //Error: s.Method1 undefined (type SuperFoo has no field or method Method1)
}

vs.

// github.com/bigwhite/experiments/go19-examples/typealias/typealias-embedding.go

package main

type Foo struct{}
type Bar = Foo

type SuperFoo struct {
    Bar
}

func (f *Foo) Method1() {
}

func main() {
    var s SuperFoo
    s.Method1() // ok
}

通过type alias得到的alias Bar在被嵌入到其他类型中,其依然携带着源类型Foo的method set

4、接口类型

接口类型的identical的定义决定了无论采用哪种方法,下面的赋值都成立:

// github.com/bigwhite/experiments/go19-examples/typealias/typealias-interface.go
package main

type MyInterface interface{
    Foo()
}

type MyInterface1 MyInterface
type MyInterface2 = MyInterface

type MyInt int

func (i *MyInt)Foo() {

}

func main() {
    var i MyInterface = new(MyInt)
    var i1 MyInterface1 = i // ok
    var i2 MyInterface2 = i1 // ok

    print(i, i1, i2)
}

5、exported type alias

前面说过type alias和源类型几乎是一样的,type alias有一个特性:可以通过声明exported type alias将package内的unexported type导出:

//github.com/bigwhite/experiments/go19-examples/typealias/typealias-export.go
package main

import (
    "fmt"

    "github.com/bigwhite/experiments/go19-examples/typealias/mylib"
)

func main() {
    f := &mylib.Foo{5, "Hello"}
    f.String()            // ok
    fmt.Println(f.A, f.B) // ok

    // Error:  f.anotherMethod undefined (cannot refer to unexported field
    // or method mylib.(*foo).anotherMethod)
    f.anotherMethod()
}

而mylib包的代码如下:

package mylib

import "fmt"

type foo struct {
    A int
    B string
}

type Foo = foo

func (f *foo) String() {
    fmt.Println(f.A, f.B)
}

func (f *foo) anotherMethod() {
}

二、Parallel Complication(并行编译)

Go 1.8版本的gc compiler的编译性能虽然照比Go 1.5刚自举时已经提升了一大截儿,但依然有提升的空间,虽然Go team没有再像Go 1.6时对改进compiler性能那么关注。

在Go 1.9中,在原先的支持包级别的并行编译的基础上又实现了包函数级别的并行编译,以更为充分地利用多核资源。默认情况下并行编译是enabled,可以通过GO19CONCURRENTCOMPILATION=0关闭。

在aliyun ECS一个4核的vm上,我们对比了一下并行编译和关闭并行的差别:

# time GO19CONCURRENTCOMPILATION=0 go1.9beta2 build -a std

real    0m16.762s
user    0m28.856s
sys    0m4.960s

# time go1.9beta2 build -a std

real    0m13.335s
user    0m29.272s
sys    0m4.812s

可以看到开启并行编译后,gc的编译性能约提升20%(realtime)。

在我的Mac 两核pc上的对比结果如下:

$time GO19CONCURRENTCOMPILATION=0 go build -a std

real    0m16.631s
user    0m36.401s
sys    0m8.607s

$time  go build -a std

real    0m14.445s
user    0m36.366s
sys    0m7.601s

提升大约13%。

三、”./…”不再匹配vendor目录

自从Go 1.5引入vendor机制以来,Go的包依赖问题有所改善,但在vendor机制的细节方面依然有很多提供的空间。

比如:我们在go test ./…时,我们期望仅执行我们自己代码的test,但Go 1.9之前的版本会匹配repo下的vendor目录,并将vendor目录下的所有包的test全部执行一遍,以下面的repo结构为例:

$tree vendor-matching/
vendor-matching/
├── foo.go
├── foo_test.go
└── vendor
    └── mylib
        ├── mylib.go
        └── mylib_test.go

如果我们使用go 1.8版本,则go test ./…输出如下:

$go test ./...
ok      github.com/bigwhite/experiments/go19-examples/vendor-matching    0.008s
ok      github.com/bigwhite/experiments/go19-examples/vendor-matching/vendor/mylib    0.009s

我们看到,go test将vendor下的包的test一并执行了。关于这点,gophers们在go repo上提了很多issue,但go team最初并没有理会这个问题,只是告知用下面的解决方法:

$go test $(go list ./... | grep -v /vendor/)

不过在社区的强烈要求下,Go team终于妥协了,并承诺在Go 1.9中fix该issue。这样在Go 1.9中,你会看到如下结果:

$go test ./...
ok      github.com/bigwhite/experiments/go19-examples/vendor-matching    0.008s

这种不再匹配vendor目录的行为不仅仅局限于go test,而是适用于所有官方的go tools。

四、GC性能

GC在Go 1.9中依旧继续优化和改善,大多数程序使用1.9编译后都能得到一定程度的性能提升。1.9 release note中尤其提到了大内存对象分配性能的显著提升。

在”go runtime metrics“搭建一文中曾经对比过几个版本的GC,从我的这个个例的图中来看,Go 1.9与Go 1.8在GC延迟方面的指标性能相差不大:

img{512x368}

五、其他

下面是Go 1.9的一些零零碎碎的改进,这里也挑我个人感兴趣的说说。

1、Go 1.9的新安装方式

go 1.9的安装增加了一种新方式,至少beta版支持,即通过go get&download安装:

# go get golang.org/x/build/version/go1.9beta2

# which go1.9beta2
/root/.bin/go18/bin/go1.9beta2
# go1.9beta2 version
go1.9beta2: not downloaded. Run 'go1.9beta2 download' to install to /root/sdk/go1.9beta2

# go1.9beta2 download
Downloaded 0.0% (15208 / 94833343 bytes) ...
Downloaded 4.6% (4356956 / 94833343 bytes) ...
Downloaded 34.7% (32897884 / 94833343 bytes) ...
Downloaded 62.6% (59407196 / 94833343 bytes) ...
Downloaded 84.6% (80182108 / 94833343 bytes) ...
Downloaded 100.0% (94833343 / 94833343 bytes)
Unpacking /root/sdk/go1.9beta2/go1.9beta2.linux-amd64.tar.gz ...
Success. You may now run 'go1.9beta2'

# go1.9beta2 version
go version go1.9beta2 linux/amd64

# go1.9beta2 env GOROOT
/root/sdk/go1.9beta2

go1.9 env输出支持json格式:

# go1.9beta2 env -json
{
    "CC": "gcc",
    "CGO_CFLAGS": "-g -O2",
    "CGO_CPPFLAGS": "",
    "CGO_CXXFLAGS": "-g -O2",
    "CGO_ENABLED": "1",
    "CGO_FFLAGS": "-g -O2",
    "CGO_LDFLAGS": "-g -O2",
    "CXX": "g++",
    "GCCGO": "gccgo",
    "GOARCH": "amd64",
    "GOBIN": "/root/.bin/go18/bin",
    "GOEXE": "",
    "GOGCCFLAGS": "-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build750457963=/tmp/go-build -gno-record-gcc-switches",
    "GOHOSTARCH": "amd64",
    "GOHOSTOS": "linux",
    "GOOS": "linux",
    "GOPATH": "/root/go",
    "GORACE": "",
    "GOROOT": "/root/sdk/go1.9beta2",
    "GOTOOLDIR": "/root/sdk/go1.9beta2/pkg/tool/linux_amd64",
    "PKG_CONFIG": "pkg-config"
}

2、go doc支持查看struct field的doc了

我们使用Go 1.8查看net/http包中struct Response的某个字段Status:

# go doc net/http.Response.Status
doc: no method Response.Status in package net/http
exit status 1

Go 1.8的go doc会报错! 我们再来看看Go 1.9:

# go1.9beta2 doc net/http.Response.Status
struct Response {
    Status string  // e.g. "200 OK"
}

# go1.9beta2 doc net/http.Request.Method
struct Request {
    // Method specifies the HTTP method (GET, POST, PUT, etc.).
    // For client requests an empty string means GET.
    Method string
}

3、核心库的变化

a) 增加monotonic clock支持

在2017年new year之夜,欧美知名CDN服务商CloudflareDNS出现大规模故障,导致欧美很多网站无法正常被访问。之后,Cloudflare工程师分析了问题原因,罪魁祸首就在于golang time.Now().Sub对时间的度量仅使用了wall clock,而没有使用monotonic clock,导致返回负值。而引发异常的事件则是新年夜际授时组织在全时间范围内添加的那个闰秒(leap second)。一般来说,wall clock仅用来告知时间,mnontonic clock才是用来度量时间流逝的。为了从根本上解决问题,Go 1.9在time包中实现了用monotonic clock来度量time流逝,这以后不会出现时间的“负流逝”问题了。这个改动不会影响到gopher对timer包的方法层面上的使用。

b) 增加math/bits包

在一些算法编程中,经常涉及到对bit位的操作。Go 1.9提供了高性能math/bits package应对这个问题。关于bits操作以及算法,可以看看经典著作《Hacker’s Delight》。这里就不举例了。

c) 提供了一个支持并发的Map类型

Go原生的map不是goroutine-safe的,尽管在之前的版本中陆续加入了对map并发的检测和提醒,但gopher一旦需要并发map时,还需要自行去实现。在Go 1.9中,标准库提供了一个支持并发的Map类型:sync.Map。sync.Map的用法比较简单,这里简单对比一下builtin map和sync.Map在并发环境下的性能:

我们自定义一个简陋的支持并发的类型:MyMap,来与sync.Map做对比:

// github.com/bigwhite/experiments/go19-examples/benchmark-for-map/map_benchmark.go
package mapbench

import "sync"

type MyMap struct {
    sync.Mutex
    m map[int]int
}

var myMap *MyMap
var syncMap *sync.Map

func init() {
    myMap = &MyMap{
        m: make(map[int]int, 100),
    }

    syncMap = &sync.Map{}
}

func builtinMapStore(k, v int) {
    myMap.Lock()
    defer myMap.Unlock()
    myMap.m[k] = v
}

func builtinMapLookup(k int) int {
    myMap.Lock()
    defer myMap.Unlock()
    if v, ok := myMap.m[k]; !ok {
        return -1
    } else {
        return v
    }
}

func builtinMapDelete(k int) {
    myMap.Lock()
    defer myMap.Unlock()
    if _, ok := myMap.m[k]; !ok {
        return
    } else {
        delete(myMap.m, k)
    }
}

func syncMapStore(k, v int) {
    syncMap.Store(k, v)
}

func syncMapLookup(k int) int {
    v, ok := syncMap.Load(k)
    if !ok {
        return -1
    }

    return v.(int)
}

func syncMapDelete(k int) {
    syncMap.Delete(k)
}

针对上面代码,我们写一些并发的benchmark test,用伪随机数作为key:

// github.com/bigwhite/experiments/go19-examples/benchmark-for-map/map_benchmark_test.go
package mapbench

import "testing"

func BenchmarkBuiltinMapStoreParalell(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        r := rand.New(rand.NewSource(time.Now().Unix()))
        for pb.Next() {
            // The loop body is executed b.N times total across all goroutines.
            k := r.Intn(100000000)
            builtinMapStore(k, k)
        }
    })
}

func BenchmarkSyncMapStoreParalell(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        r := rand.New(rand.NewSource(time.Now().Unix()))
        for pb.Next() {
            // The loop body is executed b.N times total across all goroutines.
            k := r.Intn(100000000)
            syncMapStore(k, k)
        }
    })
}
... ...

我们执行一下benchmark:

$go test -bench=.
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/experiments/go19-examples/benchmark-for-map
BenchmarkBuiltinMapStoreParalell-4         3000000           515 ns/op
BenchmarkSyncMapStoreParalell-4            2000000           754 ns/op
BenchmarkBuiltinMapLookupParalell-4        5000000           396 ns/op
BenchmarkSyncMapLookupParalell-4          20000000            60.5 ns/op
BenchmarkBuiltinMapDeleteParalell-4        5000000           392 ns/op
BenchmarkSyncMapDeleteParalell-4          30000000            59.9 ns/op
PASS
ok      github.com/bigwhite/experiments/go19-examples/benchmark-for-map    20.550s

可以看出,除了store,lookup和delete两个操作,sync.Map都比我自定义的粗糙的MyMap要快好多倍,似乎sync.Map对read做了特殊的优化(粗略看了一下代码:在map read这块,sync.Map使用了无锁机制,这应该就是快的原因了)。

d) 支持profiler labels

通用的profiler有时并不能完全满足需求,我们时常需要沿着“业务相关”的执行路径去Profile。Go 1.9在runtime/pprof包、go tool pprof工具增加了对label的支持。Go team成员rakyll有一篇文章“Profiler labels in go”详细介绍了profiler labels的用法,可以参考,这里不赘述了。

六、后记

正在写这篇文章之际,Russ Cox已经在GopherCon 2017大会上做了”The Future of Go”的演讲,并announce Go2大幕的开启,虽然只是号召全世界的gopher们一起help and plan go2的设计和开发。同时,该演讲的文字版已经在Go官网发布了,文章名为《Toward Go 2》,显然这又是Go语言演化史上的一个里程碑的时刻,值得每个gopher为之庆贺。不过Go2这枚靴子真正落地还需要一段时间,甚至很长时间。当下,我们还是要继续使用和改善Go1,就让我们从Go 1.9开始吧^0^。

本文涉及的demo代码可以在这里下载。


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系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