Go 1.5中值得關注的幾個變化
在GopherCon2015開幕之 際,Google Go Team終于放出了Go 1.5Beta1版本的安裝包。在go 1.5Beta1的發布說明中,Go Team也誠懇地承認Go 1.5將打破之前6個月一個版本的發布周期,這是因為Go 1.5變動太大,需要更多時間來準備這次發布(fix bug, Write doc)。關于Go 1.5的變化,之前Go Team staff在各種golang技術會議的slide 中暴露不少,包括:
- 編譯器和運行時由C改為Go(及少量匯編語言)重寫,實現了Go的self Bootstrap(自舉)
- Garbage Collector優化,大幅降低GC延遲(Stop The World),實現Gc在單獨的goroutine中與其他user goroutine并行運行。
- 標準庫變更以及一些go tools的引入。
每項變動都會讓gopher激動不已。但之前也只是激動,這次beta1出來后,我們可以實際體會一下這些變動帶來的“快感”了。Go 1.5beta1的發布文檔目前還不全,有些地方還有“待補充”字樣,可能與最終go 1.5發布時的版本有一定差異,不過大體內容應該是固定不變的了。這篇文章就想和大家一起淺顯地體驗一下go 1.5都給gophers們帶來了哪些變化吧。
一、語言
【map literal】
go 1.5依舊兼容Go 1 language specification,但修正了之前的一個“小疏忽”。
Go 1.4及之前版本中,我們只能這么來寫代碼:
//testmapliteral.go
package main
import (
"fmt"
)
type Point struct {
x int
y int
}
func main() {
var sl = []Point{{3, 4}, {5, 6}}
var m = map[Point]string{
Point{3,4}:"foo1",
Point{5,6}:"foo2",
}
fmt.Println(sl)
fmt.Println(m)
}
可以看到,對于Point這個struct來說,在初始化一個slice時,slice value literal中無需顯式的帶上元素類型Point,即
var sl = []Point{{3, 4}, {5, 6}}
而不是
var sl = []Point{Point{3, 4}, Point{5, 6}}
但當Point作為map類型的key類型時,初始化map時則要顯式帶上元素類型Point。Go team承認這是當初的一個疏忽,在本次Go 1.5中將該問題fix掉了。也就是說,下面的代碼在Go 1.5中可以順利編譯通過:
func main() {
var sl = []Point{{3, 4}, {5, 6}}
var m = map[Point]string{
{3,4}:"foo1",
{5,6}:"foo2",
}
fmt.Println(sl)
fmt.Println(m)
}
【GOMAXPROCS】
就像這次GopherCon2015上現任Google Go project Tech Lead的Russ Cox的開幕Keynote中所說的那樣:Go目標定位于高度并發的云環境。Go 1.5中將標識并發系統線程個數的GOMAXPROCS的初始值由1改為了運行環境的CPU核數。
// testgomaxprocs.go
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println(runtime.GOMAXPROCS(-1))
fmt.Println(runtime.NumGoroutine())
}
這個代碼在Go 1.4下(Mac OS X 4核)運行結果是:
$go run testgomaxprocs.go
1
4
而在go 1.5beta1下,結果為:
$go run testgomaxprocs.go
4
4
二、編譯
【簡化跨平臺編譯】
1.5之前的版本要想實現跨平臺編譯,需要到$GOROOT/src下重新執行一遍make.bash,執行前設置好目標環境的環境變量(GOOS和 GOARCH),Go 1.5大大簡化這個過程,使得跨平臺編譯幾乎與普通編譯一樣簡單。下面是一個簡單的例子:
//testcrosscompile.go
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println(runtime.GOOS)
}
在我的Mac上,本地編譯執行:
$go build -o testcrosscompile_darwin testcrosscompile.go
$testcrosscompile_darwin
darwin
跨平臺編譯linux amd64上的目標程序:
$GOOS=linux GOARCH=amd64 go build -o testcrosscompile_linux testcrosscompile.go
上傳testcrosscompile_linux到ubuntu 14.04上執行:
$testcrosscompile_linux
linux
雖然從用戶角度跨平臺編譯命令很簡單,但事實是go替你做了很多事情,我們可以通過build -x -v選項來輸出編譯的詳細過程,你會發現go會先進入到$GOROOT/src重新編譯runtime.a以及一些平臺相關的包。編譯輸出的信息 太多,這里就不貼出來了。但在1.5中這個過程非常快(10秒以內),與1.4之前版本的跨平臺編譯相比,完全不是一個級別,這也許就是編譯器用Go重寫完的好處之一吧。
除了直接使用go build,我們還可以使用go tool compile和go tool link來編譯程序,實際上go build也是調用這兩個工具完成編譯過程的。
$go tool compile testcrosscompile.go
testcrosscompile.o
$go tool link testcrosscompile.o
a.out
$a.out
darwin
go 1.5移除了以前的6a,6l之類的編譯連接工具,將這些工具整合到go tool中。并且go tool compile的輸出默認改為.o文件,鏈接器輸出默認改為了a.out。
【動態共享庫】
個人不是很贊同Go語言增加對動態共享庫的支持,.so和.dll這類十多年前的技術在如今內存、磁盤空間都“非常大”的前提下,似乎已經失去了以往的魅 力。并且動態共享庫所帶來的弊端:"DLL hell"會讓程序后續的運維痛苦不已。Docker等輕量級容器的興起,面向不變性的架構(immutable architecture)受到更多的關注。人們更多地會在container這一層進行操作,一個純static link的應用在部署和維護方面將會有天然優勢,.so只會增加復雜性。如果單純從與c等其他語言互操作的角度,似乎用途也不會很廣泛(但游戲或ui領域 可能會用到)。不過go 1.5還是增加了對動態鏈接庫的支持,不過從go tool compile和link的doc說明來看,目前似乎還處于實驗階段。
既然go 1.5已經支持了shared library,我們就來實驗一下。我們先規劃一下測試repository的目錄結構:
$GOPATH
/src
/testsharedlib
/shlib
– lib.go
/app
/main.go
lib.go中的代碼很簡單:
//lib.go
package shlib
import "fmt"
// export Method1
func Method1() {
fmt.Println("shlib -Method1")
}
對于希望導出的方法,采用export標記。
我們來將這個lib.go編譯成shared lib,注意目前似乎只有linux平臺支持編譯go shared library:
$ go build -buildmode=shared testsharedlib/shlib
# /tmp/go-build709704006/libtestsharedlib-shlib.so
warning: unable to find runtime/cgo.a
編譯ok,那個warning是何含義不是很理解。
要想.so被其他go程序使用,需要將.so安裝到相關目錄下。我們install一下試試:
$ go install -buildmode=shared testsharedlib/shlib
multiple roots /home1/tonybai/test/go/go15/pkg/linux_amd64_dynlink & /home1/tonybai/.bin/go15beta1/go/pkg/linux_amd64_dynlink
go工具居然糾結了,不知道選擇放在哪里,一個是$GOPATH/pkg/linux_amd64_dynlink,另外一個則是$GOROOT/pkg/linux_amd64_dynlink,我不清楚這是不是一個bug。
在Google了之后,我嘗試了網上的一個解決方法,先編譯出runtime的動態共享庫:
$go install -buildmode=shared runtime sync/atomic
編譯安裝后,你就會在$GOROOT/pkg下面看到多出來一個目錄:linux_amd64_dynlink。這個目錄下的結構如下:
$ ls -R
.:
libruntime,sync-atomic.so runtime runtime.a runtime.shlibname sync
./runtime:
cgo.a cgo.shlibname
./sync:
atomic.a atomic.shlibname
這里看到了之前warning提到的runtime/cgo.a,我們再來重新執行一下build,看看能不能消除warning:
$ go build -buildmode=shared testsharedlib/shlib
# /tmp/go-build086398801/libtestsharedlib-shlib.so
/home1/tonybai/.bin/go15beta1/go/pkg/tool/linux_amd64/link: cannot implicitly include runtime/cgo in a shared library
這回連warnning都沒有了,直接是一個error。這里提示:無法在一個共享庫中隱式包含runtime/cgo。也就是說我們在構建 testshared/shlib這個動態共享庫時,還需要顯式的link到runtime/cgo,這里就需要另外一個命令行標志:- linkshared。我們再來試試:
$ go build -linkshared -buildmode=shared testsharedlib/shlib
這回build成功!我們再來試試install:
$ go install -linkshared -buildmode=shared testsharedlib/shlib
同樣成功了。并且我們在$GOPATH/pkg/linux_amd64_dynlink下發現了共享庫:
$ ls -R
.:
libtestsharedlib-shlib.so testsharedlib
./testsharedlib:
shlib.a shlib.shlibname
$ ldd libtestsharedlib-shlib.so
linux-vdso.so.1 => (0x00007fff93983000)
libruntime,sync-atomic.so => /home1/tonybai/.bin/go15beta1/go/pkg/linux_amd64_dynlink/libruntime,sync-atomic.so (0x00007fa150f1b000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa150b3f000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fa150921000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa1517a7000)
好了,既然共享庫編譯出來了。我們就來用一下這個共享庫。
//app/main.go
package main
import (
"testsharedlib/shlib"
)
func main() {
shlib.Method1()
}
$ go build -linkshared main.go
$ ldd main
linux-vdso.so.1 => (0x00007fff579f7000)
libruntime,sync-atomic.so => /home1/tonybai/.bin/go15beta1/go/pkg/linux_amd64_dynlink/libruntime,sync-atomic.so (0x00007fa8d6df2000)
libtestsharedlib-shlib.so => /home1/tonybai/test/go/go15/pkg/linux_amd64_dynlink/libtestsharedlib-shlib.so (0x00007fa8d6962000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa8d6586000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fa8d6369000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa8d71ef000)
$ main
shlib -Method1
編譯執行ok。從輸出結果來看,我們可以清晰看到main依賴的.so以及so的路徑。我們再來試試,如果將testsharedlib源碼目錄移除后,是否還能編譯ok:
$ go build -linkshared main.go
main.go:4:2: cannot find package "testsharedlib/shlib" in any of:
/home1/tonybai/.bin/go15beta1/go/src/testsharedlib/shlib (from $GOROOT)
/home1/tonybai/test/go/go15/src/testsharedlib/shlib (from $GOPATH)
go編譯器無法找到shlib,也就說即便是動態鏈接,我們也要有動態共享庫的源碼,應用才能編譯通過。
【internal package】
internal包不是go 1.5的原創,在go 1.4中就已經提出對internal package的支持了。但go 1.4發布時,internal package只能用于GOROOT下的go core核心包,用戶層面GOPATH不支持internal package。按原計劃,go 1.5中會將internal包機制工作范圍全面擴大到所有repository的。我原以為1.5beta1以及將internal package機制生效了,但實際結果呢,我們來看看示例代碼:
測試目錄結構如下:
testinternal/src
mypkg/
/internal
/foo
foo.go
/pkg1
main.go
otherpkg/
main.go
按照internal包的原理,預期mypkg/pkg1下的代碼是可以import "mypkg/internal/foo"的,otherpkg/下的代碼是不能import "mypkg/internal/foo"的。
//foo.go
package foo
import "fmt"
func Foo() {
fmt.Println("mypkg/internal/foo")
}
//main.go
package main
import "mypkg/internal/foo"
func main() {
foo.Foo()
}
在pkg1和otherpkg下分別run main.go:
mypkg/pkg1$ go run main.go
mypkg/internal/foo
otherpkg$ go run main.go
mypkg/internal/foo
可以看到在otherpkg下執行時,并沒有任何build error出現。看來internal機制并未生效。
我們再來試試import $GOROOT下某些internal包,看看是否可以成功:
package main
import (
"fmt"
"image/internal/imageutil"
)
func main() {
fmt.Println(imageutil.DrawYCbCr)
}
我們run這個代碼:
$go run main.go
0x6b7f0
同樣沒有出現任何error。
不是很清楚為何在1.5beta1中internal依舊無效。難道非要等最終1.5 release版么?
【Vendor】
Vendor機制是go team為了解決go第三方包依賴和管理而引入的實驗性技術。你執行以下go env:
$go env
GOARCH="amd64"
GOBIN="/Users/tony/.bin/go15beta1/go/bin"
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/tony/Test/GoToolsProjects"
GORACE=""
GOROOT="/Users/tony/.bin/go15beta1/go"
GOTOOLDIR="/Users/tony/.bin/go15beta1/go/pkg/tool/darwin_amd64"
GO15VENDOREXPERIMENT=""
CC="clang"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fno-common"
CXX="clang++"
CGO_ENABLED="1"
從結果中你會看到新增一個GO15VENDOREXPERIMENT變量,這個就是用來控制vendor機制是否開啟的環境變量,默認不開啟。若要開啟,可以在環境變量文件中設置或export GO15VENDOREXPERIMENT=1臨時設置。
vendor機制是在go 1.5beta1發布前不長時間臨時決定加入到go 1.5中的,Russ Cox在Keith Rarick之前的一個Proposal的基礎上重新做了設計而成,大致機制內容:
If there is a source directory d/vendor, then,
when compiling a source file within the subtree rooted at d,
import "p" is interpreted as import "d/vendor/p" if that exists.
When there are multiple possible resolutions,
the most specific (longest) path wins.
The short form must always be used: no import path can
contain “/vendor/” explicitly.
Import comments are ignored in vendored packages.
下面我們來測試一下這個機制。首先我們臨時開啟vendor機制,export GO15VENDOREXPERIMENT=1,我們的測試目錄規劃如下:
testvendor
vendor/
tonybai.com/
foolib/
foo.go
main/
main.go
$GOPATH/src/tonybai.com/foolib/foo.go
//vendor/tonybai.com/foolib/foo.go
package foo
import "fmt"
func Hello() {
fmt.Println("foo in vendor")
}
//$GOPATH/src/tonybai.com/foolib/foo.go
package foo
import "fmt"
func Hello() {
fmt.Println("foo in gopath")
}
vendor和gopath下的foo.go稍有不同,主要在輸出內容上,以方便后續區分。
現在我們編譯執行main.go
//main/main.go
package main
import (
"tonybai.com/foolib"
)
func main() {
foo.Hello()
}
$go run main.go
foo in gopath
顯然結果與預期不符,我們通過go list -json來看main.go的依賴包路徑:
$go list -json
{
… …
"Imports": [
"tonybai.com/foolib"
],
"Deps": [
"errors",
"fmt",
"io",
"math",
"os",
"reflect",
"runtime",
"strconv",
"sync",
"sync/atomic",
"syscall",
"time",
"tonybai.com/foolib",
"unicode/utf8",
"unsafe"
]
}
可以看出并沒有看到vendor路徑,main.go import的是$GOPATH下的foo。難道是go 1.5beta1的Bug?于是翻看各種資料,最后在go 1.5beta1發布前最后提交的revison的commit log中得到了幫助:
cmd/go: disable vendoredImportPath for code outside $GOPATH
It was crashing.
This fixes the build for
GO15VENDOREXPERIMENT=1 go test -short runtime
Fixes #11416.
Change-Id: I74a9114cdd8ebafcc9d2a6f40bf500db19c6e825
Reviewed-on: https://go-review.googlesource.com/11964
Reviewed-by: Russ Cox <rsc@golang.org>
從commit log來看,大致意思是$GOPATH之外的代碼的vendor機制被disable了(因為某個bug)。也就是說只有$GOPATH路徑下的包在 import時才會考慮vendor路徑,我們的代碼的確沒有在$GOPATH下,我們重新設置一下$GOPATH。
$export GOPATH=~/test/go/go15
[tony@TonydeMacBook-Air-2 ~/test/go/go15/src/testvendor/main]$go list -json
{
… …
"Imports": [
"testvendor/vendor/tonybai.com/foolib"
],
"Deps": [
"errors",
"fmt",
"io",
"math",
"os",
"reflect",
"runtime",
"strconv",
"sync",
"sync/atomic",
"syscall",
"testvendor/vendor/tonybai.com/foolib",
"time",
"unicode/utf8",
"unsafe"
]
}
這回可以看到vendor機制生效了。執行main.go:
$go run main.go
foo in vendor
這回與預期結果就相符了。
前面提到,關閉GOPATH外的vendor機制是因為一個bug,相信go 1.5正式版發布時,這塊會被enable的。
三、小結
Go 1.5還增加了很多工具,如trace,但因文檔不全,尚不知如何使用。
Go 1.5標準庫也有很多小的變化,這個只有到使用時才能具體深入了解。
Go 1.5更多是Go語言骨子里的變化,也就是runtime和編譯器重寫。語法由于兼容Go 1,所以基本frozen,因此從外在看來,基本沒啥變動了。
至于Go 1.5的性能,官方的說法是,有的程序用1.5編譯后會變得慢點,有的會快些。官方bench的結果是總體比1.4快上一些。但Go 1.5在性能方面主要是為了減少gc延遲,后續版本才會在性能上做進一步優化,優化空間還較大的,這次runtime、編譯器由c變go,很多地方的go 代碼并非是最優的,多是自動翻譯,相信經過Go team的優化后,更idiomatic的Go code會讓Go整體性能更為優異。
來自:http://tonybai.com/2015/07/10/some-changes-in-go-1-5/