這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
在Go 1.3發布半年過去後,Go核心項目組於本月初發布了Go 1.4 Beta1版本。這個版本的幾個變化點雖然不是革命性的,但對後續Go語言的發展來說,打下了基礎,定下了基調。這裡就幾個值得關注的變化點結合Go 1.4代碼進行一些簡單描述,希望大家能對Go 1.4有個感性的認知和瞭解。
Go 1.4依舊保持了Go 1相容性的承諾,你的已有代碼幾乎無需任何改動就可以通過Go 1.4的編譯並運行。(以下是我的測試環境:go version go1.3 darwin/amd64 vs. go version go1.4beta1 linux/amd64)
一、語言變化
1、For-range迴圈
在Go 1.3及以前,for-range迴圈具有兩種形式:
for k, v := range x {
…
}
和
for k := range x {
…
}
問題:如果我們不關心迴圈中的值,我們只關心迴圈本身,我們仍然要提供一個變數,或用_佔位。
for _ = range x {
…
}
下面這樣的文法在Go 1.3及以前是無法編譯通過的:
for range x {
…
}
不過Go 1.4支援這種形式的文法,它使得代碼更加clean,雖然它可能很少會被使用到。
例子:
//testforrange.go
package main
import "fmt"
func main() {
var a [5]int = [5]int{2, 3, 4, 5, 6}
for k, v := range a {
fmt.Println(k, v)
}
for k := range a {
fmt.Println(k)
}
for _ = range a {
fmt.Println("print without care about the key and value")
}
for range a {
fmt.Println("new syntax – print without care about the key and value")
}
}
Go 1.3編譯出錯:
$go run testforrange.go
# command-line-arguments
./testforrange.go:19: syntax error: unexpected range, expecting {
./testforrange.go:22: syntax error: unexpected }
Go 1.4編譯成功並輸出正確結果:
0 2
1 3
2 4
3 5
4 6
0
1
2
3
4
print without care about the key and value
print without care about the key and value
print without care about the key and value
print without care about the key and value
print without care about the key and value
new syntax – print without care about the key and value
new syntax – print without care about the key and value
new syntax – print without care about the key and value
new syntax – print without care about the key and value
new syntax – print without care about the key and value
2、通過**T調用方法
下面這個例子:
package main
import "fmt"
type T int
func (T) M() {
fmt.Println("Call M")
}
var x **T
func main() {
x.M()
}
按照Go 1.4官方release note的說法,1.3版本及以前的gc和gccgo都會正常接受這種調用方式。但Go 1規範只允許自動在x前面加一個解引用,而不是兩個,因此這個是有悖於定義的。Go 1.4強制禁止這種調用。
不過根據我實際的測試,Go 1.3和Go 1.4針對上面代碼都會出現同樣地編譯錯誤。
$go run testdoubledeferpointer.go
# command-line-arguments
./testdoubledeferpointer.go:14: calling method M with receiver x (type **T) requires explicit dereference
二、支援的作業系統以及處理器體系架構的變化
這個無法示範。不過一個主要的變化就是Go 1.4可以構建出運行於ARM處理器Android作業系統上的二進位程式了。使用go.mobile庫中的支援包,Go 1.4也可以構建出可以被Android應用載入的.so庫。
三、相容性變化
人們通過unsafe包並利用Go的內部實現細節和資料的機器表示形式來繞過Go語言類型系統的約束。Go的設計者們認為這是對Go相容性規範的 不尊重,在Go 1.4中,Go核心組正式宣布unsafe code不再保證其相容性。這次Go 1.4並沒有針對此做任何代碼變動,只是一個clarification而已。
四、實現和工具的變化
1、運行時(runtime)的變化
Go 1.3及以前版本,Go語言的runtime(垃圾收集、並發支援、interface管理、maps、slices、strings等)主要由C語言和 少量組合語言實現的。在1.4版本中,很多代碼被替換成了用Go自身實現,這樣記憶體回收行程可以掃描程式運行時棧,擷取活躍變數的精確資訊。這個變 化很大,但對程式應該沒有語義上的影響。
這次重寫使得記憶體回收行程變得更加精確,這意味著它知道所有程式中活躍指標的位置。這些相關改變將減小heap的大小,總體上大約減少 10%~30%。
這樣做的結果是棧也不再需要是分段的(segmented)了,消除了“hot split”的問題。如果一個stack到達了使用上限,Go將分配一個新的更大的stack,相應goroutine中的所有活躍的棧幀將被複製到新 stack上,所有指向棧的指標將被更新。在某些情境下,其效能將會變得顯著提升,並且這樣修改後,其效能更具可預測性。
連續棧(contiguous stacks)的使用使得棧的初始Size可以更小,在Go 1.4中goroutine的初始棧大小從8192位元組縮小為2048位元組。(正式發布時也許會改為4096)。
interface實值型別的實現也做了調整。在之前的發布版中,interface值內部用一個字(word)來承載,要麼是一個指標,要麼是一 個單字(one-word)大小的純量值,這取決於interface值變數中具體儲存的是什麼對象。這個實現會給垃圾收集器帶來諸多困難,因此 在Go 1.4版本中interface值內部就用指標表示。在啟動並執行程式中,絕大多數interface值都是指標,因此這個影響很小。不過那些在 interface實值型別變數中儲存整型值的程式將會有更多的記憶體配置。
2、gccgo的狀態
Gcc和Go兩個項目的發布計劃不是同步的,GCC 4.9版本包含了實現了1.2規範的gccgo,下一個發布版gcc 5.0將可能包含實現了1.4規範的gccgo。
3、internal包(內部包)
Go以package為基本邏輯單元組織代碼。Go 1.3及之前版本的Go語言實際上只支援兩種形式Package內符號的可見度:本地的(unexported)和全域的(exported)。有些時候 我們希望一些包並非能被所有外部包所匯入,但卻能被其“臨近”的包所匯入和訪問。但之前的Go語言不具備這種特性。Go 1.4引入了"internal"包的概念,匯入這種internal包的規則約束如下:
如果匯入代碼本身不在以"internal"目錄的父目錄為root的分類樹中,那麼 不允許其匯入路徑(import path)中包含internal元素。
例如:
– a/b/c/internal/d/e/f只可以被以a/b/c為根的分類樹下的代碼匯入,不能被a/b/g下的代碼匯入。
– $GOROOT/src/pkg/internal/xxx只能被標準庫($GOROOT/src)中的代碼所匯入。(註:Go 1.4 取消了$GOROOT/src/pkg,標準庫都移到$GOROOT/src下了)。
– $GOROOT/src/pkg/net/http/internal只能被net/http和net/http/*的包所匯入
– $GOPATH/src/mypkg/internal/foo只能被$GOPATH/src/mypkg包的代碼所匯入
對於Go 1.4該規則首先強制應用於$GOROOT下。Go 1.5將擴充應用到$GOPATH下。
4、權威匯入路徑(import paths)
我們經常使用託管在公用代碼託管服務中的代碼,諸如github.com,這意味著包匯入路徑包含託管服務名,比如github.com/rsc /pdf。一些情境下為了不破壞使用者代碼,我們用rsc.io/pdf,屏蔽底層具體哪家託管服務,比如rso.io/pdf的背後可能是 github.com也可能是bitbucket。但這樣會引入一個問題,那就是不經意間我們為一個包產生了兩個合法的匯入路徑。如果一個程式中 使用了這兩個合法路徑,一旦某個路徑沒有被識別出有更新,或者將包遷移到另外一個不同的託管公用服務下去時,使用舊匯入路徑包的程式就會報錯。
Go 1.4引入一個包字句的注釋,用於標識這個包的權威匯入路徑。如果使用的匯入的路徑不是權威路徑,go命令會拒絕編譯。文法很簡單:
package pdf // import "rsc.io/pdf"
如果pdf包使用了權威匯入路徑注釋,那麼那些嘗試使用github.com/rsc/pdf匯入路徑的程式將會被go編譯器拒絕編譯。
這個權威匯入路徑檢查是在編譯期進行的,而不是下載階段。
我們舉個例子:
我們的包foo以前是放在github.com/bigwhite/foo下面的,後來主託管站換成了tonybai.com/foo,最新的 foo包的代碼:
package foo // import "tonybai.com/foo"
import "fmt"
func Echo(a string) {
fmt.Println("Foo:, a)
}
某個應用通過舊路徑github.com/bigwhite/foo匯入了該包:
//testcanonicalimportpath.go
package main
import "github.com/bigwhite/foo"
func main() {
foo.Echo("Hello!")
}
我們編譯該go檔案,得到以下結果:
code in directory /home/tonybai/Test/Go/src/github.com/bigwhite/foo expects import "tonybai.com/foo"
5、go generate子命令
go 1.4中go工具集合新引入一個子命令:go generate,用於在編譯前自動化產生某類代碼。例如在.y上運行yacc編譯器產生實現該文法的.go源檔案。或是使用stringer工 具自動為常量產生String方法。這個命令並非由go tools(build, get等)自動執行,而必須顯式執行。
不過我簡單測試了一下,似乎這個命令設計文檔中的:
// +build generate
並不好用啊。即便將其作為generate directive放入go源檔案,該檔案依舊會被go編譯器當做正常go檔案編譯。Go 1.4標準庫中使用go generate directive的有三個地方:
strconv/quote.go://go:generate go run makeisprint.go -output isprint.go
time/zoneinfo_windows.go://go:generate go run genzabbrs.go -output zoneinfo_abbrs_windows.go
unicode/letter.go://go:generate go run maketables.go -tables=all -output tables.go
通過go generate來實現泛型(generics)似乎不那麼優雅啊。雖然設計者並非將其作為Go泛型的實現^_^。
6、源碼布局變化
在Go自身源碼庫($GOROOT下)中,包的源碼放在src/pkg中,這樣做與其他庫不同,包括Go自己的子庫,比如go.tools。因此在Go 1.4中,pkg這一層分類樹將被去除,比如fmt包的源碼曾經放在src/pkg/fmt下,現在則放在src/fmt下。
五、效能
絕大多數程式使用1.4編譯後的運行速度會與1.3的一致或略有提升,有些可能也會變得慢些。這次修改的較多,很難準確預測。
這次許多runtime的代碼由C變為Go,這將導致一些heap大小有所縮減。另外這樣做後有利於Go編譯器的最佳化,諸如內聯,會帶來效能上的小幅提升。
記憶體回收行程一方面得到了加速,使得重度依賴垃圾收集的程式得到可衡量的提升。但另外一方面,新的write barrier又引起了效能下降。提升和下降的量的多少取決於程式的行為。
2014, bigwhite. 著作權.