這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Go語言在2016年當選tiobe index的年度程式設計語言。
轉眼間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^。
一、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-go、vscode-go)以及各種tools的版本。
官方對type alias的定義非常簡單:
An alias declaration binds an identifier to the given type.
我們怎麼來理解新增的type alias和傳統的type definition的區別呢?
type T1 T2 // 傳統的type defintionvs.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.gopackage main// type definitionstype MyInt inttype MyInt1 MyIntfunc 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.gopackage mainimport "fmt"// type aliastype MyInt = inttype MyInt1 = MyIntfunc 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.gopackage main// type definitionstype MyInt inttype MyInt1 MyIntfunc (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.gopackage maintype Foo struct{}type Bar = Foofunc (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.gopackage maintype MyInt = intfunc (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.gopackage maintype Foo struct{}type Bar Footype 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.gopackage maintype Foo struct{}type Bar = Footype 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.gopackage maintype MyInterface interface{ Foo()}type MyInterface1 MyInterfacetype MyInterface2 = MyInterfacetype MyInt intfunc (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.gopackage mainimport ( "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 mylibimport "fmt"type foo struct { A int B string}type Foo = foofunc (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 stdreal 0m16.762suser 0m28.856ssys 0m4.960s# time go1.9beta2 build -a stdreal 0m13.335suser 0m29.272ssys 0m4.812s
可以看到開啟並行編譯後,gc的編譯效能約提升20%(realtime)。
在我的Mac 兩核pc上的對比結果如下:
$time GO19CONCURRENTCOMPILATION=0 go build -a stdreal 0m16.631suser 0m36.401ssys 0m8.607s$time go build -a stdreal 0m14.445suser 0m36.366ssys 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.008sok 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延遲方面的指標效能相差不大:
五、其他
下面是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 versiongo1.9beta2: not downloaded. Run 'go1.9beta2 download' to install to /root/sdk/go1.9beta2# go1.9beta2 downloadDownloaded 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 versiongo 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.Statusdoc: no method Response.Status in package net/httpexit status 1
Go 1.8的go doc會報錯! 我們再來看看Go 1.9:
# go1.9beta2 doc net/http.Response.Statusstruct Response { Status string // e.g. "200 OK"}# go1.9beta2 doc net/http.Request.Methodstruct 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服務商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包的方法層面上的使用。
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.gopackage mapbenchimport "sync"type MyMap struct { sync.Mutex m map[int]int}var myMap *MyMapvar syncMap *sync.Mapfunc 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.gopackage mapbenchimport "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: darwingoarch: amd64pkg: github.com/bigwhite/experiments/go19-examples/benchmark-for-mapBenchmarkBuiltinMapStoreParalell-4 3000000 515 ns/opBenchmarkSyncMapStoreParalell-4 2000000 754 ns/opBenchmarkBuiltinMapLookupParalell-4 5000000 396 ns/opBenchmarkSyncMapLookupParalell-4 20000000 60.5 ns/opBenchmarkBuiltinMapDeleteParalell-4 5000000 392 ns/opBenchmarkSyncMapDeleteParalell-4 30000000 59.9 ns/opPASSok 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
2017, bigwhite. 著作權.