標籤:
Golang在變數聲明、初始化以及指派陳述式上照比C語言有了許多改進:
a) 支援在同一行聲明多個變數
var a, b, c int
b) 支援在同一行初始化多個變數(不同類型也可以)
var a, b, c = 5, "hello", 3.45
a, b, c := 5, "hello", 3.45 (short variable declaration)
c) 支援在同一行對多個變數進行賦值(在聲明後且不同類型也可以)
a, b, c = 5, "hello", 3.45
這種文法糖我們是笑納的,畢竟人生苦短,少寫一行是一行啊^_^。
但這種文法糖卻給我們帶來了一些令人困惑的問題!比如下面這個就是Rob Pike在一個talk中slide(Go Course Day2)中的一個問題:
n0, n1 = n0 + n1, n0
or:
n0, n1 = op(n0,n1), n0
n0, n1的值在上述語句執行完畢後到底為多少呢?
顯然這個問題涉及到Go語言的語句求值順序(evaluation order)。求值序在任何一門程式設計語言中都是比較難纏的,很多情形下,語言規範給出的答案都是“undefined(未定義)” or "not specified" or “依賴實現”,尤其是對於哪些模稜兩可的寫法,就如Rob Pike給出的那個問題。
我們要想搞清楚Go中的求值順序,我們需要求助於Go language specification,Go spec與Go發行版一起發布,你可以啟動一個godoc web server(godoc -http=:6060,然後訪問localhost:6060/ref/spec)查看go language specification。Go language specification專門有一個小節/ref/spec#Order_of_evaluation對求值順序做了說明。
在Go specs中,有這樣三點陳述:
1、變數聲明(variable declaration)中的初始設定式(initialization expressions)的求值順序(evaluation order)由初始化依賴(initialization dependencies)決定;但對於初始設定式內部的運算元的求值需要按照2中的順序:從左至右;
2、在非變數初始化語句中,對錶達式、指派陳述式或返回語句中的運算元進行求值時,運算元中包含的函數(function)調用、方法(method)調用和通訊操作(主要針對channel)將按文法從左至右的順序求值。
3、指派陳述式求值分為兩個階段,第一階段是等號左邊的index expressions、pointer indirections和等號右邊的運算式中的運算元的求值順序按照2中從左至右的順序;第二階段按從左至右的順序對變數賦值。
下面我們就分別理解一下這三點。
一、變數聲明中初始設定式的求值順序
帶初始設定式的變數聲明的形式如下:
var a, b, c = expr1, expr2, expr3 //包層級或函數/方法內部
or
a, b, c := expr1, expr2, expr3 //僅函數/方法內部
根據lang specs說明,求值順序是由初始化依賴(initialization dependencies)規則決定的。那初始化依賴規則是什麼呢?在Golang specs中也有專門章節說明:ref/spec#Package_initialization。
初始化依賴規則總結一下,大致有如下幾條:
1、包中,包層級變數的初始化順序按照聲明先後的順序,但如果某個變數(比如a)的初始設定式中依賴其他變數(比如b),那麼變數a的初始化順序在變數b後面。
2、對於未初始化的,且不含有對應初始設定式或其初始設定式不依賴任何未初始設定變數的變數,我們稱之為"ready for initialization"變數。初始化就是按照聲明順序重複執行對下一個變數的初始化過程,直到沒有"ready for initialization"變數為止。
3、如果初始化過程完畢後依然有變數處於未初始化狀態,那程式有語法錯誤。
4、多個處於不同檔案中的變數的聲明順序依賴編譯器處理檔案的順序,先處理的檔案中的變數的聲明順序先於後處理的檔案中的所有變數。
5、依賴分析以包為單位執行,只有位於同一個包中的被依賴的變數、函數、方法才會被考慮。
規則是抽象難懂的,例子更直觀易理解,我們看一個golang spec中的例子,並使用上述規則進行分析。實驗環境:go 1.5, amd64,Darwin Kernel Version 13.1.0。
//golang-statements-evaluating-order/example1.go
package main
import "fmt"
var (
a = c + b
b = f()
c = f()
d = 3
)
func f() int {
d++
return d
}
func main() {
fmt.Println(a, b, c, d)
}
我們來分析一下程式執行後的a, b, c, d四個變數的結果值,不過不同的初始化順序會導致結果值不同,因此分析四個變數的初始化順序是至關重要的。
變數a, b, c, d的初始化過程如下:
1、根據規則,初始化按照變數聲明先後順序進行,因此先來分析變數a,a初始設定式依賴b 和c;因此變數a的初始化次序排在b、c的後面;
2、按照a的初始化右值運算式,c、b在右值運算式中的出現順序是c先於b;
3、c是否是一個ready for initialization變數呢?我們看到c依賴f這個函數,而f這個函數則依賴變數d的初始化,因此d排在c之前;
4、我們來看變數d,"d = 3",d未初始化且不含有初始設定式,因此d是一個ready for initialization變數,我們可以從d開始初始化了。至此四個變數的初始化順序排定 d -> c -> b -> a;(這塊兒與spec中分析有差異,但從運行結果來看,應該是這個順序;關於這個spec的issue參見#12369)
5、d初始化為3,此時已初始設定變數集合[d=3];
6、接著初始化c:c = f(),因此c = 4(此時d=4),此時已初始設定變數集合[c=4,d=4];
7、接下來輪到b:b = f(),因此b = 5 (此時d = 5),此時已初始設定變數集合[b=5,c=4,d=5];
8、最後初始化a: a = c + b,在已初始設定變數集合中我們可以找到b和c,因此a= 9,這樣四個變數到此均已初始化;
9、經過分析:程式執行的結果應該是9,5,4,5。
我們來執行一下這個程式,驗證一下我們的分析結果是否正確:
$go run example1.go
9 5 4 5
我們再來看一個例子,也是golang specs中的例子,我們稍作改造,並把它設定為example2:
//golang-statements-evaluating-order/example2.go
package main
import "fmt"
var a, b, c = f() + v(), g(), sqr(u()) + v()
func f() int {
fmt.Println("calling f")
return c
}
func g() int {
fmt.Println("calling g")
return a
}
func sqr(x int) int {
fmt.Println("calling sqr")
return x * x
}
func v() int {
fmt.Println("calling v")
return 1
}
func u() int {
fmt.Println("calling u")
return 2
}
func main() {
fmt.Println(a, b, c)
}
同樣根據變數初始化依賴規則對這個例子進行分析:
1、按照變數聲明順序,先初始化a:a= f() + v(),f()依賴變數c;v不依賴任何變數,因此變數c的初始化順序應該在a變數前:c -> a。
2、分析c:c = sqr(u()) + v();u、sqr、v三個函數不依賴任何變數,因此c處於ready for initialization,於是對c進行初始化,函數執行順序(從左至右)為:u() -> sqr() -> v(); 此時已初始設定變數集合:[c = 5];
3、回到a:a = f() + v(),c初始化後,a也處理ready for initialization,於是對a初始化,函數執行順序為:f() -> v(),此時已初始設定變數集合:[c=5, a= 6];
4、按照變數聲明次序,接下來輪到變數b:b= g(),而g()依賴a,a已經初始化完畢了,因此b也是ready for initialization,於是對b初始化,函數執行次序為:g(),至此已初始設定變數集合:[c=5, a=6, b=6]。
5、經過分析:程式執行的結果應該是6,6,5。
我們來執行一下這個程式,驗證一下我們的分析結果是否正確:
$go run example2.go
calling u
calling sqr
calling v
calling f
calling v
calling g
6 6 5
二、非變數初始化語句中的求值順序
前面提到過:在非變數初始化語句中,對錶達式、指派陳述式或返回語句中的運算元進行求值時,運算元中包含的函數(function)調用、方法(method)調用和通訊操作(主要針對channel)將按文法從左至右的順序求值。
我們同樣來看一個例子:example3.go。
//golang-statements-evaluating-order/example3.go
package main
import "fmt"
func f() int {
fmt.Println("calling f")
return 1
}
func g(a, b, c int) int {
fmt.Println("calling g")
return 2
}
func h() int {
fmt.Println("calling h")
return 3
}
func i() int {
fmt.Println("calling i")
return 1
}
func j() int {
fmt.Println("calling j")
return 1
}
func k() bool {
fmt.Println("calling k")
return true
}
func main() {
var y = []int{11, 12, 13}
var x = []int{21, 22, 23}
var c chan int = make(chan int)
go func() {
c <- 1
}()
y[f()], _ = g(h(), i()+x[j()], <-c), k()
fmt.Println(y)
}
y[f()], _ = g(h(), i()+x[j()], <-c), k() 這行語句是指派陳述式,但指派陳述式的運算元中包含函數調用、channel操作,按照規則,這些函數調用、channel操作按從左至右順序估值。
1、按照從左至右順序,第一個是y[f()]中的f();
2、接下來是g(),g()的參數列表依然是一個賦值操作,因此其涉及到的函數調用順序為h(), i(),j(),<-c,因此實際上的順序為h() –> i()–> j() –> c操作 -> g();
3、最後是k(),因此完整的調用順序是:f()-> h() –> i()–> j() –> c操作 -> g() –> k()。
實際運行情況如下:
$go run example3.go
calling f
calling h
calling i
calling j
calling g
calling k
[11 2 13]
三、指派陳述式的求值順序
我們再回到前面Rob Pike那個問題:
n0, n1 = n0 + n1, n0
or:
n0, n1 = op(n0, n1), n0
這是一個指派陳述式,根據規則3,我們對等號兩端的運算式的運算元採用從左至右的求值順序。
我們假定初值:
n0, n1 = 1, 2
1、第一階段:等號兩端運算式求值,上述問題中,只有右端有n0+n1和n0兩個運算式,但運算式的運算元(n0,n1)都是初始化過後的了,因此直接將值帶入,得到求值結果。求值後,語句可以看成:n0, n1 = 3, 1;
2、第二階段:賦值。n0 =3, n1 = 1
//golang-statements-evaluating-order/example4.go
package main
import "fmt"
func example1() {
n0, n1 := 1, 2
n0, n1 = n0+n1, n0
fmt.Println(n0, n1)
}
func op(a, b int) int {
return a + b
}
func example2() {
n0, n1 := 1, 2
n0, n1 = op(n0, n1), n0
fmt.Println(n0, n1)
}
func main() {
example1()
example2()
}
$go run example4.go
3 1
3 1
四、小結
雖說理解了規則,但實際工作中我們還是盡量不要寫出像:"var a, b, c = f() + v(), g(), sqr(u()) + v()"這樣複雜、難以讓人理解的語句。必要的話,拆分成多行就好了,還可以增加些代碼量(如果你的公司是以代碼量為評價績效指標之一的),得饒人處且饒人啊,燒腦的語句還是盡量避免為好。
go學習筆記1--變數聲明