转眼间 6 个月过去了,Go 在 tiobe index 排行榜上继续强势攀升,在最新公布的 TIBOE INDEX 7 月份的排行榜上,Go 挺进 Top10:
还有不到一个月, 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^。
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-go、vscode-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 时,实际用的就是 孙悟空 这个类型,因为卡卡罗特就是孙悟空,孙悟空就是卡卡罗特。
我们用几个小例子再来仔细对比一下:
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)
- }
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)
- }
有了上面关于类型方法的结果,其实我们也可以直接知道在类型 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。
接口类型的 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)
- }
前面说过 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() {}
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%。
自从 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 在 Go 1.9 中依旧继续优化和改善,大多数程序使用 1.9 编译后都能得到一定程度的性能提升。1.9 release note 中尤其提到了大内存对象分配性能的显著提升。
在 "go runtime metrics" 搭建一文中曾经对比过几个版本的 GC,从我的这个个例的图中来看,Go 1.9 与 Go 1.8 在 GC 延迟方面的指标性能相差不大:
下面是 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"
- }
我们使用 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
- }
在 2017 年 new year 之夜,欧美知名 CDN 服务商 Cloudflare 的 DNS 出现大规模故障 ,导致欧美很多网站无法正常被访问。之后,Cloudflare 工程师分析了问题原因,罪魁祸首就在于 golang time.Now().Sub 对时间的度量仅使用了 wall clock,而没有使用 monotonic clock,导致返回负值。而引发异常的事件则是新年夜际授时组织在全时间范围内添加的那个闰秒 (leap second)。一般来说,wall clock 仅用来告知时间,mnontonic clock 才是用来度量时间流逝的。为了从根本上解决问题,Go 1.9 在 time 包中实现了 用 monotonic clock 来度量 time 流逝 ,这以后不会出现时间的 "负流逝" 问题了。这个改动不会影响到 gopher 对 timer 包的方法层面上的使用。
在一些算法编程中,经常涉及到对 bit 位的操作。Go 1.9 提供了高性能 math/bits package 应对这个问题。关于 bits 操作以及算法,可以看看经典著作 《Hacker's Delight》 。这里就不举例了。
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 使用了无锁机制,这应该就是快的原因了)。
通用的 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
© 2017,bigwhite. 版权所有.
来源: http://www.tuicool.com/articles/vmmqYb6