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代碼可以在這裏下載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章