标签 RobPike 下的文章

Go 1.11中值得关注的几个变化

转眼间又近年底,距8月25日Go 1.11版本正式发布已过去快三个月了。由于种种原因,Go语言发布变化系列的Go 1.11版本没能及时放出。近期网课发布上线后,个人时间压力稍缓和。又恰看到近期Go 1.12 release note的initial version已经加入到master,于是这篇文章便上升到个人Todo list的Top3的位置,我也尽一切可能的碎片时间收集素材,撰写文章内容。这个时候谈Go 1.11,总有炒“冷饭”的嫌疑,虽然这碗饭还有一定温度^_^。

一. Go 1.11版本的重要意义

在Go 1.11版本之前的Go user官方调查中,Gopher抱怨最多的三大问题如下:

  • 包依赖管理
  • 缺少泛型
  • 错误处理

而Go 1.11开启了问题1:包依赖管理解决的实验。这表明了社区的声音在影响Go语言演化的过程中扮演着日益重要的角色了。

同时,Go 1.11Russ CoxGopherCon 2017大会上发表 “Toward Go2″之后的第一个Go版本,是为后续“Go2”的渐进落地奠定基础的一个版本。

二. Go 1.11版本变化概述

在”Go2″声音日渐响亮的今天,兼容性(compatibility)也依旧是Go team考虑的Go语言演化的第一原则,这一点通过Rob Pike在9月份的Go Sydney Meetup上的有关Go 2 Draft SpecificationsTalk可以证明(油管视频)。

img{512x368}
兼容性依然是”Go2″的第一考虑

Go 1.11也一如既往版本那样,继续遵守着Go1兼容协议,这意味使用从Go1.0到Go1.10编写的代码理论上依旧可以通过Go 1.11版本编译并正常运行。

随着Go 1.11版本的发布,一些老版本的操作系统将不再被支持,比如Windows XP、macOS 10.9.x等。不被支持不意味着完全不能用,只是Go 1.11在这些老旧os上运行时出现问题将不被官方support了。同时根据Go的release support规定,Go 1.11发布也同时意味着Go 1.9版本将和之前的older go release版本一样,官方将不再提供支持了(关键bug fix、security problem fix等)。

Go 1.11中为近两年逐渐兴起的RISC-Vcpu架构预留了GOARCH值:riscv和riscv64。

Go 1.11中为调试器增加了一个新的实验功能,那就是允许在调试过程中动态调用Go函数,比如在断点处调用String方法等。Delve 1.1.0及以上版本可以使用该功能。

在运行时方面,Go 1.11使用了一个稀疏heap布局,这样就去掉了以往Go heap最大512G的限制。

通过Go 1.11编译的Go程序一般来说性能都会有小幅的提升。对于使用math/big包的程序或arm64架构上的Go程序而言,这次的提升尤为明显。

Go 1.11中最大的变化莫过于两点:

  • module机制的实验性引入,以试图解决长久以来困扰Gopher们的包依赖问题;
  • 增加对WebAssembly的支持,这样以后Gopher们可以通过Go语言编写前端应用了。

Go 1.11的change很多,这是core team和社区共同努力的结果。但在我这个系列文章中,我们只能详细关注少数重要的变化。下面我们就来稍微详细地说说go module和go support WebAssembly这两个显著的变化。

三. go module

在Go 1.11 beta2版本发布之前,我曾经基于当时的Go tip版本撰写了一篇 《初窥go module》的文章,重点描述了go module的实现机制,包括Semantic Import VersioningMinimal Version Selection等,因此对go module(前身为vgo)是什么以及实现机制感兴趣的小伙伴儿们可以先移步到那篇文章了解。在这里我将通过为一个已存在的repo添加go.mod的方式来描述go module。

这里我们使用的是go 1.11.2版本,repo为gocmpp。注意:我们没有显式设置GO111MODULE的值,这样只有在GOPATH之外的路径下,且当前路径下有go.mod或子路径下有go.mod文件时,go compiler才进入module-aware模式(相比较于gopath模式)。

1. 初始化go.mod

我们先把gocmpp clone到gopath之外的一个路径下:

# git clone https://github.com/bigwhite/gocmpp.git
Cloning into 'gocmpp'...
remote: Enumerating objects: 1, done.
remote: Counting objects: 100% (1/1), done.
remote: Total 950 (delta 0), reused 0 (delta 0), pack-reused 949
Receiving objects: 100% (950/950), 3.85 MiB | 0 bytes/s, done.
Resolving deltas: 100% (396/396), done.
Checking connectivity... done.

在应用go module之前,我们先来在传统的gopath模式下build一次:

# go build
connect.go:24:2: cannot find package "github.com/bigwhite/gocmpp/utils" in any of:
    /root/.bin/go1.11.2/src/github.com/bigwhite/gocmpp/utils (from $GOROOT)
    /root/go/src/github.com/bigwhite/gocmpp/utils (from $GOPATH)

正如我们所料,由于处于GOPATH外面,且GO111MODULE并未显式设置,Go compiler会尝试在当前目录或子目录下查找go.mod,如果没有go.mod文件,则会采用传统gopath模式编译,即在$GOPATH/src下面找相关的import package,因此失败。

下面我们通过建立go.mod,将编译mode切换为module-aware mode。

我们通过go mod init命令来为gocmpp创建go.mod文件:

# go mod init github.com/bigwhite/gocmpp
go: creating new go.mod: module github.com/bigwhite/gocmpp

# cat go.mod
module github.com/bigwhite/gocmpp

我们看到,go mod init命令在当前目录下创建一个go.mod文件,内有一行内容,描述了该module为 github.com/bigwhite/gocmpp。

我们再来构建一下gocmpp:

# go build
go: finding golang.org/x/text/transform latest
go: finding golang.org/x/text/encoding/unicode latest
go: finding golang.org/x/text/encoding/simplifiedchinese latest
go: finding golang.org/x/text v0.3.0
go: finding golang.org/x/text/encoding latest
go: downloading golang.org/x/text v0.3.0

由于当前目录下有了go.mod文件,go compiler将工作在module-aware模式下,自动分析gocmpp的依赖、确定gocmpp依赖包的初始版本,并下载这些版本的依赖包缓存到特定目录下(目前是存放在$GOPATH/pkg/mod下面)

# cat go.mod
module github.com/bigwhite/gocmpp

require golang.org/x/text v0.3.0

我们看到go.mod中多了一行信息:“require golang.org/x/text v0.3.0″。这就是gocmpp这个module所依赖的第三方包以及经过go compiler初始分析确定使用的版本(v0.3.0)。

2. 用于verify的go.sum

go build后,当前目录下还多出了一个go.sum文件。

# cat go.sum
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

go.sum记录每个依赖库的版本和对应的内容的校验和(一个哈希值)。每当增加一个依赖项时,如果go.sum中没有,则会将该依赖项的版本和内容校验和添加到go.sum中。go命令会使用这些校验和与缓存在本地的依赖包副本元信息(比如:$GOPATH/pkg/mod/cache/download/golang.org/x/text/@v下面的v0.3.0.ziphash)进行比对校验。

如果我修改了$GOPATH/pkg/mod/cache/download/golang.org/x/text/@v/v0.3.0.ziphash中的值,那么当我执行下面verify命令时会报错:

# go mod verify
golang.org/x/text v0.3.0: zip has been modified (/root/go/pkg/mod/cache/download/golang.org/x/text/@v/v0.3.0.zip)
golang.org/x/text v0.3.0: dir has been modified (/root/go/pkg/mod/golang.org/x/text@v0.3.0)

如果没有“恶意”修改,则verify会报成功:

# go mod verify
all modules verified

3. 用why解释为何依赖,给出依赖路径

go.mod中的依赖项由go相关命令自动生成和维护。但是如果开发人员想知道为什么会依赖某个package,可以通过go mod why命令来查询原因。go mod why命令默认会给出一个main包到要查询的packge的最短依赖路径。如果go mod why使用 -m flag,则后面的参数将被看成是module,并给出main包到每个module中每个package的最短依赖路径(如果依赖的话):

下面我们通过go mod why命令查看一下gocmpp module到 golang.org/x/oauth2和golang.org/x/exp两个包是否有依赖:

# go mod why golang.org/x/oauth2 golang.org/x/exp
go: finding golang.org/x/oauth2 latest
go: finding golang.org/x/exp latest
go: downloading golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288
go: downloading golang.org/x/exp v0.0.0-20181112044915-a3060d491354
go: finding golang.org/x/net/context/ctxhttp latest
go: finding golang.org/x/net/context latest
go: finding golang.org/x/net latest
go: downloading golang.org/x/net v0.0.0-20181114220301-adae6a3d119a
# golang.org/x/oauth2
(main module does not need package golang.org/x/oauth2)

# golang.org/x/exp
(main module does not need package golang.org/x/exp)

通过结尾几行的输出日志,我们看到gocmpp的main package没有对golang.org/x/oauth2和golang.org/x/exp两个包产生任何依赖。

我们加上-m flag再来执行一遍:

# go mod why -m golang.org/x/oauth2 golang.org/x/exp
# golang.org/x/oauth2
(main module does not need module golang.org/x/oauth2)

# golang.org/x/exp
(main module does not need module golang.org/x/exp)

同样是没有依赖的输出结果,但是输出日志中使用的是module,而不是package字样。说明go mod why将golang.org/x/oauth2和golang.org/x/exp视为module了。

我们再来查询一下对golang.org/x/text的依赖:

# go mod why golang.org/x/text
# golang.org/x/text
(main module does not need package golang.org/x/text)

# go mod why -m golang.org/x/text
# golang.org/x/text
github.com/bigwhite/gocmpp/utils
golang.org/x/text/encoding/simplifiedchinese

我们看到,如果-m flag不开启,那么gocmpp main package没有对golang.org/x/text的依赖路径;如果-m flag开启,则golang.org/x/text被视为module,go mod why会检查gocmpp main package到module: golang.org/x/text下面所有package是否有依赖路径。这里我们看到gocmpp main package依赖了golang.org/x/text module下面的golang.org/x/text/encoding/simplifiedchinese这个package,并给出了最短依赖路径。

4. 清理go.mod和go.sum中的条目:go mod tidy

经过上述操作后,我们再来看看go.mod中的内容:

# cat go.mod
module github.com/bigwhite/gocmpp

require (
    github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048
    golang.org/x/net v0.0.0-20181114220301-adae6a3d119a // indirect
    golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 // indirect
    golang.org/x/text v0.3.0
)

我们发现go.mod中require block增加了许多条目,显然我们的gocmpp并没有依赖到golang.org/x/oauth2和golang.org/x/net中的任何package。我们要清理一下go.mod,使其与gocmpp源码中的第三方依赖的真实情况保持一致,我们使用go mod tidy命令:

# go mod tidy
# cat go.mod
module github.com/bigwhite/gocmpp

require (
    github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048
    golang.org/x/text v0.3.0
)

# cat go.sum
github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048 h1:3O5zXlWvrRdioniMPz8pW+pGi+BNEFRtVhvj0GnknbQ=
github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

我们看到:执行完tidy命令后,go.mod和go.sum都变得简洁了,里面的每一个条目都是gocmpp所真实依赖的package/module的信息。

5. 对依赖包的版本进行“升降级”(upgrade或downgrade)

如果对go mod init初始选择的依赖包版本不甚满意,或是第三方依赖包有更新的版本发布,我们日常开发工作中都会进行对对依赖包的版本进行“升降级”(upgrade或downgrade)的操作。在go module模式下,如何来做呢?由于go.mod和go.sum是由go compiler管理的,这里不建议手工去修改go.mod中require中module的版本号。我们可以通过module-aware的go get命令来实现我们的目的。

我们先来查看一下golang.org/x/text都有哪些版本可用:

# go list -m -versions golang.org/x/text
golang.org/x/text v0.1.0 v0.2.0 v0.3.0

我们选择将golang.org/x/text从v0.3.0降级到v0.1.0:

# go get golang.org/x/text@v0.1.0
go: finding golang.org/x/text v0.1.0
go: downloading golang.org/x/text v0.1.0

降级后,我们test一下:

# go test
PASS
ok      github.com/bigwhite/gocmpp    0.003s

我们这时再看看go.mod和go.sum:

# cat go.mod
module github.com/bigwhite/gocmpp

require (
    github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048
    golang.org/x/text v0.1.0
)

# cat go.sum
github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048 h1:3O5zXlWvrRdioniMPz8pW+pGi+BNEFRtVhvj0GnknbQ=
github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
golang.org/x/text v0.1.0 h1:LEnmSFmpuy9xPmlp2JeGQQOYbPv3TkQbuGJU3A0HegU=
golang.org/x/text v0.1.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

go.mod中依赖的golang.org/x/text已经从v0.3.0自动变成了v0.1.0了。go.sum中也增加了golang.org/x/text v0.1.0的条目,不过v0.3.0的条目依旧存在。我们可以通过go mod tidy清理一下:

# go mod tidy
# cat go.sum
github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048 h1:3O5zXlWvrRdioniMPz8pW+pGi+BNEFRtVhvj0GnknbQ=
github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
golang.org/x/text v0.1.0 h1:LEnmSFmpuy9xPmlp2JeGQQOYbPv3TkQbuGJU3A0HegU=
golang.org/x/text v0.1.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

go 1.11中的go get也是支持两套工作模式的: 一套是传统gopath mode的;一套是module-aware的。

如果我们在gopath之外的路径,且该路径下没有go.mod,那么go get还是回归gopath mode:

# go get golang.org/x/text@v0.1.0
go: cannot use path@version syntax in GOPATH mode

而module-aware的go get在前面已经演示过了,这里就不重复演示了。

在module-aware模式下,go get -u会更新依赖,升级到依赖的最新minor或patch release。比如:我们在gocmpp module root path下执行:

# go get -u golang.org/x/text
# cat go.mod
module github.com/bigwhite/gocmpp

require (
    github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048
    golang.org/x/text v0.3.0 //恢复到0.3.0
)

我们看到刚刚降级回v0.1.0的依赖项又自动变回v0.3.0了(注意仅minor号变更)。

如果仅仅要升级patch号,而不升级minor号,可以使用go get -u=patch A 。比如:如果golang.org/x/text有v0.1.1版本,那么go get -u=patch golang.org/x/text会将go.mod中的text后面的版本号变为v0.1.1,而不是v0.3.0。

如果go get后面不接具体package,则go get仅针对于main package。

处于module-aware工作模式下的go get更新某个依赖(无论是升版本还是降版本)时,会自动计算并更新其间接依赖的包的版本。

6. 兼容go 1.11之前版本的reproduceable build: 使用vendor

处于module-aware mode下的go compiler是完全不理会vendor目录的存在的,go compiler只会使用$GOPATH/pkg/mod下(当前go mod缓存的包是放在这个位置,也许将来会更换位置)缓存的第三方包的特定版本进行编译构建。那么这样一来,对于采用go 1.11之前版本的go compiler来说,reproduceable build就失效了。

为此,go mod提供了vendor子命令,可以根据依赖在module顶层目录自动生成vendor目录:

# go mod vendor -v
# github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048
github.com/dvyukov/go-fuzz/gen
# golang.org/x/text v0.3.0
golang.org/x/text/encoding/simplifiedchinese
golang.org/x/text/encoding/unicode
golang.org/x/text/transform
golang.org/x/text/encoding
golang.org/x/text/encoding/internal
golang.org/x/text/encoding/internal/identifier
golang.org/x/text/internal/utf8internal
golang.org/x/text/runes

gopher可以将vendor目录提交到git repo,这样老版本的go compiler就可以使用vendor进行reproduceable build了。

当然在module-aware mode下,go 1.11 compiler也可以使用vendor进行构建,使用下面命令即可:

go build -mod=vendor

注意在上述命令中,只有位于module顶层路径的vendor才会起作用。

7. 国内gopher如何适应go module

对于国内gopher来说,下载go get package的经历并不是总是那么愉快!尤其是get golang.org/x/xxx路径下的package的时候。以golang.org/x/text为例,在传统的gopath mode下,我们还可以通过下载github.com/golang/text,然后在本地将路径改为golang.org/x/text的方式来获取text相关包。但是在module-aware mode下,对package的下载和本地缓存管理完全由go tool自动完成,国内的gopher们该如何应对呢?

两种方法:
1. 用go.mod中的replace语法,将golang.org/x/text指向本地另外一个目录下已经下载好的github.com/golang/text
2. 使用GOPROXY

方法1显然具有临时性,本地改改第三方依赖库代码,用于调试还可以;第二种方法显然是正解,我们通过一个proxy来下载那些在qiang外的package。Microsoft工程师开源的athens项目正是一个用于这个用途的go proxy工具。不过限于篇幅,这里就不展开说明了。我将在后续文章详细谈谈 go proxy的,尤其是使用athens实现go proxy的详细方案。

四. 对WebAssembly的支持

1. 简介

由于长期在后端浸淫,对javascript、WebAssembly等前端的技能了解不多,因此这里对Go支持WebAssembly也就能介绍个梗概。下图是对Go支持WebAssembly的一个粗浅的理解:

img{512x368}

我们看到满足WebAssembly标准要求的wasm运行于browser之上,类比于一个amd64架构的binary program运行于linux操作系统之上。我们在x86-64的linux上执行go build,实质执行的是:

GOOS=linux GOARCH=amd64 go build ...

因此为了将Go源码编译为wasm,我们需要执行:

GOOS=js GOARCH=wasm go build ...

同时, _js.go和 *_wasm.go这样的文件也和_linux.go、*_amd64.go一样,会被go compiler做特殊处理。

2. 一个hello world级别的WebAssembly的例子

例子来自Go官方Wiki,代码结构如下:

/Users/tony/test/Go/wasm/hellowasm git:(master) ✗ $tree
.
├── hellowasm.go
├── index.html
└── server.go

hellowasm.go是最终wasm应用对应的源码:

// hellowasm.go

package main

import "fmt"

func main() {
    fmt.Println("Hello, WebAssembly!")
}

我们先将其编译为wasm文件main.wasm:

$GOOS=js GOARCH=wasm go build -o main.wasm hellowasm.go
$ls -F
hellowasm.go    index.html    main.wasm*    server.go

接下来我们从Goroot下面copy一个javascript支持文件wasm_exec.js:

cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .

我们建立index.html,并在该文件中使用wasm_exec.js,并加载main.wasm:

//index.html
<html>
        <head>
                <meta charset="utf-8">
                <script src="wasm_exec.js"></script>
                <script>
                        const go = new Go();
                        WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
                                go.run(result.instance);
                        });
                </script>
        </head>
        <body></body>
</html>

最后,我们建立server.go,这是一个File server:

//server.go
package main

import (
    "flag"
    "log"
    "net/http"
)

var (
    listen = flag.String("listen", ":8080", "listen address")
    dir    = flag.String("dir", ".", "directory to serve")
)

func main() {
    flag.Parse()
    log.Printf("listening on %q...", *listen)
    err := http.ListenAndServe(*listen, http.FileServer(http.Dir(*dir)))
    log.Fatalln(err)
}

启动该server:

$go run server.go
2018/11/19 21:19:17 listening on ":8080"...

打开Chrome浏览器,右键打开Chrome的“检查”页面,访问127.0.0.1:8080,我们将在console(控制台)窗口看到下面内容:

img{512x368}

我们看到”Hello, WebAssembly”字样输出到console上了!

3. 使用node.js执行wasm应用

wasm应用除了可以运行于支持WebAssembly的浏览器上之外,还可以通过node.js运行它。

我的实验环境中安装的node版本是:

$node -v
v9.11.1

我们删除server.go,然后执行下面命令:

$GOOS=js GOARCH=wasm go run -exec="$(go env GOROOT)/misc/wasm/go_js_wasm_exec" .
Hello, WebAssembly!

我们看到通过go_js_wasm_exec命令我们成功通过node执行了main.wasm。

不过每次通过go run -exec来执行,命令行太长,不易记住和使用。我们将go_js_wasm_exec放到$PATH下面,然后直接执行go run:

 $export PATH=$PATH:"$(go env GOROOT)/misc/wasm"
 $which go_js_wasm_exec
/Users/tony/.bin/go1.11.2/misc/wasm/go_js_wasm_exec
$GOOS=js GOARCH=wasm go run .
Hello, WebAssembly!

main.wasm同样被node执行,并且这样执行main.wasm程序的命令行长度大大缩短了!

五. 小结

从Go 1.11版本开始,Go语言开始驶入“语言演化”的深水区。Go语言究竟该如何演化?如何在保持语言兼容性、社区不分裂的前提下,满足社区对于错误处理、泛型等语法特性的需求,是摆在Go设计者面前的一道难题。但我相信,无论Go如何演化,Go设计者都会始终遵循Go语言安身立命的那几个根本原则,也是大多数Gopher喜欢Go的根本原因:兼容、简单、可读和高效。


我的网课“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