Go 1.5中值得關注的幾個變化

jopen 9年前發布 | 35K 次閱讀 Go

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/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!