标签 Windows 下的文章

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://51smspush.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有很多优点,比如:简单原生支持并发等,而不错的可移植性也是Go被广大程序员接纳的重要因素之一。但你知道为什么Go语言拥有很好的平台可移植性吗?本着“知其然,亦要知其所以然”的精神,本文我们就来探究一下Go良好可移植性背后的原理。

一、Go的可移植性

说到一门编程语言可移植性,我们一般从下面两个方面考量:

  • 语言自身被移植到不同平台的容易程度;
  • 通过这种语言编译出来的应用程序对平台的适应性。

Go 1.7及以后版本中,我们可以通过下面命令查看Go支持OS和平台列表:

$go tool dist list
android/386
android/amd64
android/arm
android/arm64
darwin/386
darwin/amd64
darwin/arm
darwin/arm64
dragonfly/amd64
freebsd/386
freebsd/amd64
freebsd/arm
linux/386
linux/amd64
linux/arm
linux/arm64
linux/mips
linux/mips64
linux/mips64le
linux/mipsle
linux/ppc64
linux/ppc64le
linux/s390x
nacl/386
nacl/amd64p32
nacl/arm
netbsd/386
netbsd/amd64
netbsd/arm
openbsd/386
openbsd/amd64
openbsd/arm
plan9/386
plan9/amd64
plan9/arm
solaris/amd64
windows/386
windows/amd64

从上述列表我们可以看出:从linux/arm64的嵌入式系统到linux/s390x的大型机系统,再到Windows、linux和darwin(mac)这样的主流操作系统、amd64、386这样的主流处理器体系,Go对各种平台和操作系统的支持不可谓不广泛。

Go官方似乎没有给出明确的porting guide,关于将Go语言porting到其他平台上的内容更多是在golang-dev这样的小圈子中讨论的事情。但就Go语言这么短的时间就能很好的支持这么多平台来看,Go的porting还是相对easy的。从个人对Go的了解来看,这一定程度上得益于Go独立实现了runtime。

img{512x368}

runtime是支撑程序运行的基础。我们最熟悉的莫过于libc(C运行时),它是目前主流操作系统上应用最普遍的运行时,通常以动态链接库的形式(比如:/lib/x86_64-linux-gnu/libc.so.6)随着系统一并发布,它的功能大致有如下几个:

  • 提供基础库函数调用,比如:strncpy
  • 封装syscall(注:syscall是操作系统提供的API口,当用户层进行系统调用时,代码会trap(陷入)到内核层面执行),并提供同语言的库函数调用,比如:malloc、fread等;
  • 提供程序启动入口函数,比如:linux下的__libc_start_main。

libc等c runtime lib是很早以前就已经实现的了,甚至有些老旧的libc还是单线程的。一些从事c/c++开发多年的程序员早年估计都有过这样的经历:那就是链接runtime库时甚至需要选择链接支持多线程的库还是只支持单线程的库。除此之外,c runtime的版本也参差不齐。这样的c runtime状况完全不能满足go语言自身的需求;另外Go的目标之一是原生支持并发,并使用goroutine模型,c runtime对此是无能为力的,因为c runtime本身是基于线程模型的。综合以上因素,Go自己实现了runtime,并封装了syscall,为不同平台上的go user level代码提供封装完成的、统一的go标准库;同时Go runtime实现了对goroutine模型的支持。

独立实现的go runtime层将Go user-level code与OS syscall解耦,把Go porting到一个新平台时,将runtime与新平台的syscall对接即可(当然porting工作不仅仅只有这些);同时,runtime层的实现基本摆脱了Go程序对libc的依赖,这样静态编译的Go程序具有很好的平台适应性。比如:一个compiled for linux amd64的Go程序可以很好的运行于不同linux发行版(centos、ubuntu)下。

以下测试试验环境为:darwin amd64 Go 1.8

二、默认”静态链接”的Go程序

我们先来写两个程序:hello.c和hello.go,它们完成的功能都差不多,在stdout上输出一行文字:

//hello.c
#include <stdio.h>

int main() {
        printf("%s\n", "hello, portable c!");
        return 0;
}

//hello.go
package main

import "fmt"

func main() {
    fmt.Println("hello, portable go!")
}

我们采用“默认”方式分别编译以下两个程序:

$cc -o helloc hello.c
$go build -o hellogo hello.go

$ls -l
-rwxr-xr-x    1 tony  staff     8496  6 27 14:18 helloc*
-rwxr-xr-x    1 tony  staff  1628192  6 27 14:18 hellogo*

从编译后的两个文件helloc和hellogo的size上我们可以看到hellogo相比于helloc简直就是“巨人”般的存在,其size近helloc的200倍。略微学过一些Go的人都知道,这是因为hellogo中包含了必需的go runtime。我们通过otool工具(linux上可以用ldd)查看一下两个文件的对外部动态库的依赖情况:

$otool -L helloc
helloc:
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1197.1.1)
$otool -L hellogo
hellogo:

通过otool输出,我们可以看到hellogo并不依赖任何外部库,我们将hellog这个二进制文件copy到任何一个mac amd64的平台上,均可以运行起来。而helloc则依赖外部的动态库:/usr/lib/libSystem.B.dylib,而libSystem.B.dylib这个动态库还有其他依赖。我们通过nm工具可以查看到helloc具体是哪个函数符号需要由外部动态库提供:

$nm helloc
0000000100000000 T __mh_execute_header
0000000100000f30 T _main
                 U _printf
                 U dyld_stub_binder

可以看到:_printf和dyld_stub_binder两个符号是未定义的(对应的前缀符号是U)。如果对hellog使用nm,你会看到大量符号输出,但没有未定义的符号。

$nm hellogo
00000000010bb278 s $f64.3eb0000000000000
00000000010bb280 s $f64.3fd0000000000000
00000000010bb288 s $f64.3fe0000000000000
00000000010bb290 s $f64.3fee666666666666
00000000010bb298 s $f64.3ff0000000000000
00000000010bb2a0 s $f64.4014000000000000
00000000010bb2a8 s $f64.4024000000000000
00000000010bb2b0 s $f64.403a000000000000
00000000010bb2b8 s $f64.4059000000000000
00000000010bb2c0 s $f64.43e0000000000000
00000000010bb2c8 s $f64.8000000000000000
00000000010bb2d0 s $f64.bfe62e42fefa39ef
000000000110af40 b __cgo_init
000000000110af48 b __cgo_notify_runtime_init_done
000000000110af50 b __cgo_thread_start
000000000104d1e0 t __rt0_amd64_darwin
000000000104a0f0 t _callRet
000000000104b580 t _gosave
000000000104d200 T _main
00000000010bbb20 s _masks
000000000104d370 t _nanotime
000000000104b7a0 t _setg_gcc
00000000010bbc20 s _shifts
0000000001051840 t errors.(*errorString).Error
00000000010517a0 t errors.New
.... ...
0000000001065160 t type..hash.time.Time
0000000001064f70 t type..hash.time.zone
00000000010650a0 t type..hash.time.zoneTrans
0000000001051860 t unicode/utf8.DecodeRuneInString
0000000001051a80 t unicode/utf8.EncodeRune
0000000001051bd0 t unicode/utf8.RuneCount
0000000001051d10 t unicode/utf8.RuneCountInString
0000000001107080 s unicode/utf8.acceptRanges
00000000011079e0 s unicode/utf8.first

$nm hellogo|grep " U "

Go将所有运行需要的函数代码都放到了hellogo中,这就是所谓的“静态链接”。是不是所有情况下,Go都不会依赖外部动态共享库呢?我们来看看下面这段代码:

//server.go
package main

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

func main() {
    cwd, err := os.Getwd()
    if err != nil {
        log.Fatal(err)
    }

    srv := &http.Server{
        Addr:    ":8000", // Normally ":443"
        Handler: http.FileServer(http.Dir(cwd)),
    }
    log.Fatal(srv.ListenAndServe())
}

我们利用Go标准库的net/http包写了一个fileserver,我们build一下该server,并查看它是否有外部依赖以及未定义的符号:

$go build server.go
-rwxr-xr-x    1 tony  staff  5943828  6 27 14:47 server*

$otool -L server
server:
    /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)
    /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 0.0.0, current version 0.0.0)
    /System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)

$nm server |grep " U "
                 U _CFArrayGetCount
                 U _CFArrayGetValueAtIndex
                 U _CFDataAppendBytes
                 U _CFDataCreateMutable
                 U _CFDataGetBytePtr
                 U _CFDataGetLength
                 U _CFDictionaryGetValueIfPresent
                 U _CFEqual
                 U _CFNumberGetValue
                 U _CFRelease
                 U _CFStringCreateWithCString
                 U _SecCertificateCopyNormalizedIssuerContent
                 U _SecCertificateCopyNormalizedSubjectContent
                 U _SecKeychainItemExport
                 U _SecTrustCopyAnchorCertificates
                 U _SecTrustSettingsCopyCertificates
                 U _SecTrustSettingsCopyTrustSettings
                 U ___error
                 U ___stack_chk_fail
                 U ___stack_chk_guard
                 U ___stderrp
                 U _abort
                 U _fprintf
                 U _fputc
                 U _free
                 U _freeaddrinfo
                 U _fwrite
                 U _gai_strerror
                 U _getaddrinfo
                 U _getnameinfo
                 U _kCFAllocatorDefault
                 U _malloc
                 U _memcmp
                 U _nanosleep
                 U _pthread_attr_destroy
                 U _pthread_attr_getstacksize
                 U _pthread_attr_init
                 U _pthread_cond_broadcast
                 U _pthread_cond_wait
                 U _pthread_create
                 U _pthread_key_create
                 U _pthread_key_delete
                 U _pthread_mutex_lock
                 U _pthread_mutex_unlock
                 U _pthread_setspecific
                 U _pthread_sigmask
                 U _setenv
                 U _strerror
                 U _sysctlbyname
                 U _unsetenv

通过otool和nm的输出结果我们惊讶的看到:默认采用“静态链接”的Go程序怎么也要依赖外部的动态链接库,并且也包含了许多“未定义”的符号了呢?问题在于cgo。

三、cgo对可移植性的影响

默认情况下,Go的runtime环境变量CGO_ENABLED=1,即默认开始cgo,允许你在Go代码中调用C代码,Go的pre-compiled标准库的.a文件也是在这种情况下编译出来的。在$GOROOT/pkg/darwin_amd64中,我们遍历所有预编译好的标准库.a文件,并用nm输出每个.a的未定义符号,我们看到下面一些包是对外部有依赖的(动态链接):

=> crypto/x509.a
                 U _CFArrayGetCount
                 U _CFArrayGetValueAtIndex
                 U _CFDataAppendBytes
                 ... ...
                 U _SecCertificateCopyNormalizedIssuerContent
                 U _SecCertificateCopyNormalizedSubjectContent
                 ... ...
                 U ___stack_chk_fail
                 U ___stack_chk_guard
                 U __cgo_topofstack
                 U _kCFAllocatorDefault
                 U _memcmp
                 U _sysctlbyname

=> net.a
                 U ___error
                 U __cgo_topofstack
                 U _free
                 U _freeaddrinfo
                 U _gai_strerror
                 U _getaddrinfo
                 U _getnameinfo
                 U _malloc

=> os/user.a
                 U __cgo_topofstack
                 U _free
                 U _getgrgid_r
                 U _getgrnam_r
                 U _getgrouplist
                 U _getpwnam_r
                 U _getpwuid_r
                 U _malloc
                 U _realloc
                 U _sysconf

=> plugin.a
                 U __cgo_topofstack
                 U _dlerror
                 U _dlopen
                 U _dlsym
                 U _free
                 U _malloc
                 U _realpath$DARWIN_EXTSN

=> runtime/cgo.a
                 ... ...
                 U _abort
                 U _fprintf
                 U _fputc
                 U _free
                 U _fwrite
                 U _malloc
                 U _nanosleep
                 U _pthread_attr_destroy
                 U _pthread_attr_getstacksize
                 ... ...
                 U _setenv
                 U _strerror
                 U _unsetenv

=> runtime/race.a
                 U _OSSpinLockLock
                 U _OSSpinLockUnlock
                 U __NSGetArgv
                 U __NSGetEnviron
                 U __NSGetExecutablePath
                 U ___error
                 U ___fork
                 U ___mmap
                 U ___munmap
                 U ___stack_chk_fail
                 U ___stack_chk_guard
                 U __dyld_get_image_header
                .... ...

我们以os/user为例,在CGO_ENABLED=1,即cgo开启的情况下,os/user包中的lookupUserxxx系列函数采用了c版本的实现,我们看到在$GOROOT/src/os/user/lookup_unix.go中的build tag中包含了+build cgo。这样一来,在CGO_ENABLED=1,该文件将被编译,该文件中的c版本实现的lookupUser将被使用:

// +build darwin dragonfly freebsd !android,linux netbsd openbsd solaris
// +build cgo

package user
... ...
func lookupUser(username string) (*User, error) {
    var pwd C.struct_passwd
    var result *C.struct_passwd
    nameC := C.CString(username)
    defer C.free(unsafe.Pointer(nameC))
    ... ...
}

这样来看,凡是依赖上述包的Go代码最终编译的可执行文件都是要有外部依赖的。不过我们依然可以通过disable CGO_ENABLED来编译出纯静态的Go程序:

$CGO_ENABLED=0 go build -o server_cgo_disabled server.go

$otool -L server_cgo_disabled
server_cgo_disabled:
$nm server_cgo_disabled |grep " U "

如果你使用build的 “-x -v”选项,你将看到go compiler会重新编译依赖的包的静态版本,包括net、mime/multipart、crypto/tls等,并将编译后的.a(以包为单位)放入临时编译器工作目录($WORK)下,然后再静态连接这些版本。

四、internal linking和external linking

问题来了:在CGO_ENABLED=1这个默认值的情况下,是否可以实现纯静态连接呢?答案是可以。在$GOROOT/cmd/cgo/doc.go中,文档介绍了cmd/link的两种工作模式:internal linking和external linking。

1、internal linking

internal linking的大致意思是若用户代码中仅仅使用了net、os/user等几个标准库中的依赖cgo的包时,cmd/link默认使用internal linking,而无需启动外部external linker(如:gcc、clang等),不过由于cmd/link功能有限,仅仅是将.o和pre-compiled的标准库的.a写到最终二进制文件中。因此如果标准库中是在CGO_ENABLED=1情况下编译的,那么编译出来的最终二进制文件依旧是动态链接的,即便在go build时传入-ldflags ‘extldflags “-static”‘亦无用,因为根本没有使用external linker:

$go build -o server-fake-static-link  -ldflags '-extldflags "-static"' server.go
$otool -L server-fake-static-link
server-fake-static-link:
    /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)
    /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 0.0.0, current version 0.0.0)
    /System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)

2、external linking

而external linking机制则是cmd/link将所有生成的.o都打到一个.o文件中,再将其交给外部的链接器,比如gcc或clang去做最终链接处理。如果此时,我们在cmd/link的参数中传入-ldflags ‘extldflags “-static”‘,那么gcc/clang将会去做静态链接,将.o中undefined的符号都替换为真正的代码。我们可以通过-linkmode=external来强制cmd/link采用external linker,还是以server.go的编译为例:

$go build -o server-static-link  -ldflags '-linkmode "external" -extldflags "-static"' server.go
# command-line-arguments
/Users/tony/.bin/go18/pkg/tool/darwin_amd64/link: running clang failed: exit status 1
ld: library not found for -lcrt0.o
clang: error: linker command failed with exit code 1 (use -v to see invocation)

可以看到,cmd/link调用的clang尝试去静态连接libc的.a文件,但由于我的mac上仅仅有libc的dylib,而没有.a,因此静态连接失败。我找到一个ubuntu 16.04环境:重新执行上述构建命令:

# go build -o server-static-link  -ldflags '-linkmode "external" -extldflags "-static"' server.go
# ldd server-static-link
    not a dynamic executable
# nm server-static-link|grep " U "

该环境下libc.a和libpthread.a分别在下面两个位置:

/usr/lib/x86_64-linux-gnu/libc.a
/usr/lib/x86_64-linux-gnu/libpthread.a

就这样,我们在CGO_ENABLED=1的情况下,也编译构建出了一个纯静态链接的Go程序。

如果你的代码中使用了C代码,并依赖cgo在go中调用这些c代码,那么cmd/link将会自动选择external linking的机制:

//testcgo.go
package main

//#include <stdio.h>
// void foo(char *s) {
//    printf("%s\n", s);
// }
// void bar(void *p) {
//    int *q = (int*)p;
//    printf("%d\n", *q);
// }
import "C"
import (
    "fmt"
    "unsafe"
)

func main() {
    var s = "hello"
    C.foo(C.CString(s))

    var i int = 5
    C.bar(unsafe.Pointer(&i))

    var i32 int32 = 7
    var p *uint32 = (*uint32)(unsafe.Pointer(&i32))
    fmt.Println(*p)
}

编译testcgo.go:

# go build -o testcgo-static-link  -ldflags '-extldflags "-static"' testcgo.go
# ldd testcgo-static-link
    not a dynamic executable

vs.
# go build -o testcgo testcgo.go
# ldd ./testcgo
    linux-vdso.so.1 =>  (0x00007ffe7fb8d000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fc361000000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc360c36000)
    /lib64/ld-linux-x86-64.so.2 (0x000055bd26d4d000)

五、小结

本文探讨了Go的可移植性以及哪些因素对Go编译出的程序的移植性有影响:

  • 你的程序用了哪些标准库包?如果仅仅是非net、os/user等的普通包,那么你的程序默认将是纯静态的,不依赖任何c lib等外部动态链接库;
  • 如果使用了net这样的包含cgo代码的标准库包,那么CGO_ENABLED的值将影响你的程序编译后的属性:是静态的还是动态链接的;
  • CGO_ENABLED=0的情况下,Go采用纯静态编译;
  • 如果CGO_ENABLED=1,但依然要强制静态编译,需传递-linkmode=external给cmd/link。

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

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

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

这里是 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

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats