Go以簡(jiǎn)潔著稱,,但簡(jiǎn)潔中不乏值得玩味的小細(xì)節(jié),。這些小細(xì)節(jié)不如goroutine,、interface和channel那樣"高大上",,"屌 絲"得可能不經(jīng)常被人注意到,,但它們卻對(duì)理解Go語(yǔ)言有著重要的作用,。這里想挑出一些和大家一起通過(guò)詳實(shí)的例子來(lái)逐一展開(kāi)和理解,。本文內(nèi)容較為基礎(chǔ),適合初學(xué)者,,高手可飄過(guò):) 一,、源文件字符集和字符集編碼 Go源碼文件默認(rèn)采用Unicode字符集,Unicode碼點(diǎn)(code point)和內(nèi)存中字節(jié)序列(byte sequence)的變換實(shí)現(xiàn)使用了UTF-8:一種變長(zhǎng)多字節(jié)編碼,,同時(shí)也是一種事實(shí)字符集編碼標(biāo)準(zhǔn),,為L(zhǎng)inux、MacOSX 上的默認(rèn)字符集編碼,,因此使用Linux或MacOSX進(jìn)行Go程序開(kāi)發(fā),,你會(huì)省去很多字符集轉(zhuǎn)換方面的煩惱,。但如果你是在Windows上使用 默認(rèn)編輯器編輯Go源碼文本,當(dāng)你編譯以下代碼時(shí)會(huì)遇到編譯錯(cuò)誤: //hello.go import "fmt" func main() { $ go build hello.go 這是因?yàn)閃indows默認(rèn)采用的是CP936字符集編碼,,也就是GBK編碼,,“中國(guó)人”三個(gè)字的內(nèi)存字節(jié)序列為: “d0d6 fab9 cbc8 000a” (通過(guò)iconv轉(zhuǎn)換,然后用od -x查看) 這個(gè)字節(jié)序列并非utf-8字節(jié)序列,,Go編譯器因此無(wú)法識(shí)別,。要想通過(guò)編譯,需要將該源文件轉(zhuǎn)換為UTF-8編碼格式,。 字符集編碼對(duì)字符和字符串字面值(Literal)影響最大,,在Go中對(duì)于字符串我們可以有三種寫法: 1) 字面值 var s = "中國(guó)人" 2) 碼點(diǎn)表示法 var s1 = "\u4e2d\u56fd\u4eba" or var s2 = "\U00004e2d\U000056fd\U00004eba" 3) 字節(jié)序列表示法(二進(jìn)制表示法) var s3 = "\xe4\xb8\xad\xe5\x9b\xbd\xe4\xba\xba" 這三種表示法中,除字面值轉(zhuǎn)換為字節(jié)序列存儲(chǔ)時(shí)根據(jù)編輯器保存的源碼文件編碼格式之外,,其他兩種均不受編碼格式影響,。我們可以通過(guò)逐字節(jié)輸出來(lái)查 看字節(jié)序列的內(nèi)容: fmt.Println("s byte sequence:") 二,、續(xù)行 良好的代碼style一般會(huì)要求代碼中不能有太long的代碼行,否則會(huì)影響代碼閱讀者的體驗(yàn)。在C中有續(xù)行符"\"專門用于代碼續(xù)行處理,;但在 Go中沒(méi)有專屬續(xù)行符,,如何續(xù)行需要依據(jù)Go的語(yǔ)法規(guī)則(參見(jiàn)Go spec),。 Go與C一樣,,都是以分號(hào)(";")作為語(yǔ)句結(jié)束的標(biāo)識(shí)。不過(guò)大多數(shù)情況下,,分號(hào)無(wú)需程序員手工輸入,,而是由編譯器自動(dòng)識(shí)別語(yǔ)句結(jié)束位置,并插入 分號(hào),。因此續(xù)行要選擇合法的位置,。下面代碼展示了一些合法的續(xù)行位置:(別嫌太丑,這里僅僅是展示合法位置的demo) //details-in-go/2/newline.go f func(int, func foo(int, int) (string, error) { func main() { var sum int foo(1, var i int 實(shí)際編碼中,,我們可能經(jīng)常遇到的是fmt.Printf系列方法中format string太長(zhǎng)的情況,,但由于Go不支持相鄰字符串自動(dòng)連接(concatenate),只能通過(guò)+來(lái)連接fmt字符串,,且+必須放在前一行末尾,。另外Gofmt工具會(huì)自動(dòng)調(diào)整一些不合理的續(xù)行處理,主要針對(duì) for, if等控制語(yǔ)句,。 三,、Method Set Method Set是Go語(yǔ)法中一個(gè)重要的隱式概念,在為interface變量做動(dòng)態(tài)類型賦值,、embeding struct/interface,、type alias、method expression時(shí)都會(huì)用到Method Set這個(gè)重要概念。 1,、interface的Method Set 根據(jù)Go spec,,interface類型的Method Set就是其interface(An interface type specifies a method set called its interface)。 type I interface { I的Method Set包含的就是其literal中的兩個(gè)方法:Method1和Method2,。我們可以通過(guò)reflect來(lái)獲取interface類型的 Method Set: //details-in-go/3/interfacemethodset.go import ( type I interface { func main() { 運(yùn)行結(jié)果: 2,、除interface type外的類型的Method Set 對(duì)于非interface type的類型T,其Method Set為所有receiver為T類型的方法組成,;而類型*T的Method Set則包含所有receiver為T和*T類型的方法。 // details-in-go/3/othertypemethodset.go import "./utils" type T struct { func (t T) Method1() { func (t *T) Method2() { func (t *T) Method3() { func main() { var pt *T 我們要dump出T和*T各自的Method Set,,運(yùn)行結(jié)果如下: $go run othertypemethodset.go *main.T's method sets: 可以看出類型T的Method set僅包含一個(gè)receiver類型為T的方法:Method1,,而*T的Method Set則包含了T的Method Set以及所有receiver類型為*T的Method。 如果此時(shí)我們有一個(gè)interface type如下: type I interface { 那下面哪個(gè)賦值語(yǔ)句合法呢,?合不合法完全依賴于右值類型是否實(shí)現(xiàn)了interface type I的所有方法,,即右值類型的Method Set是否包含了I的 所有方法。 var t T var i I = t or var i I = pt 編譯錯(cuò)誤告訴我們: var i I = t // cannot use t (type T) as type I in assignment: T的Method Set中只有Method1一個(gè)方法,,沒(méi)有實(shí)現(xiàn)I接口中的 Method2,,因此不能用t賦值給i;而*T實(shí)現(xiàn)了I的所有接口,,賦值合 法,。不過(guò)Method set校驗(yàn)僅限于在賦值給interface變量時(shí)進(jìn)行,無(wú)論是T還是*T類型的方法集中的方法,,對(duì)于T或*T類型變量都是可見(jiàn)且可以調(diào)用的,,如下面代碼 都是合法的: pt.Method1() 因?yàn)镚o編譯器會(huì)自動(dòng)為你的代碼做receiver轉(zhuǎn)換: pt.Method1() <=> (*pt).Method1() 很多人糾結(jié)于method定義時(shí)receiver的類型(T or *T),個(gè)人覺(jué)得有兩點(diǎn)考慮: 1) 效率 2) 是否賦值給interface變量,、以什么形式賦值 3、embeding type的Method Set 【interface embeding】 我們先來(lái)看看interface類型embeding,。例子如下: //details-in-go/3/embedinginterface.go import "./utils" type I1 interface { type I3 interface { func main() { $go run embedinginterface.go main.I2's method sets: main.I3's method sets: 可以看出嵌入interface type的interface type I3的Method Set包含了被嵌入的interface type:I1和I2的Method Set,。很多情況下,我們Go的interface type中僅包含有少量方法,,常常僅是一個(gè)Method,,通過(guò)interface type embeding來(lái)定義一個(gè)新interface,這是Go的一個(gè)慣用法,比如我們常用的io包中的Reader, Writer以及ReadWriter接口: type Reader interface { type Writer interface { type ReadWriter interface { 【struct embeding interface】 在struct中嵌入interface type后,,struct的Method Set中將包含interface的Method Set: type T struct { func (T) Method1() { } … … 輸出結(jié)果與預(yù)期一致: main.T's method sets: *main.T's method sets: 【struct embeding struct】 在struct中embeding struct提供了一種“繼承”的手段,,外部的Struct可以“繼承”嵌入struct的所有方法(無(wú)論receiver是T還是*T類型)實(shí)現(xiàn),但 Method Set可能會(huì)略有不同,??聪旅胬樱?/p> //details-in-go/3/embedingstructinstruct.go import "./utils" type T struct { func (T) InstMethod1OfT() { } func (T) InstMethod2OfT() { } func (*T) PtrMethodOfT() { } type S struct { func (S) InstMethodOfS() { } func (*S) PtrMethodOfS() { type C struct { func main() { c.InstMethod1OfT() $go run embedingstructinstruct.go *main.C's method sets: 可以看出: 同時(shí)通過(guò)例子可以看出,無(wú)論是T還是*S的方法,,C或*C類型變量均可調(diào)用(編譯器甜頭),,不會(huì)被局限在Method Set中。 4,、alias type的Method Set Go支持為已有類型定義alias type,,如: type MyInterface I 對(duì)于alias type, Method Set是如何定義的呢?我們看下面例子: //details-in-go/3/aliastypemethodset.go import "./utils" type I interface { type T struct { func (T) InstMethod() { } } type MyInterface I func main() { var t T utils.DumpMethodSet((*MyInterface)(nil)) var m MyStruct $go run aliastypemethodset.go main.T's method sets: *main.T's method sets: main.MyInterface's method sets: main.MyStruct's method set is empty! 從例子的結(jié)果上來(lái)看,,Go對(duì)于interface和struct的alias type給出了“不一致”的結(jié)果: MyInterface的Method Set與接口類型I Method Set一致,; 四,、Method Type、Method Expression,、Method Value Go中沒(méi)有class,,方法與對(duì)象通過(guò)receiver聯(lián)系在一起,我們可以為任何非builtin類型定義method: type T struct { func (t T) Get() int { return t.a } 在C++等OO語(yǔ)言中,,對(duì)象在調(diào)用方法時(shí),,編譯器會(huì)自動(dòng)在方法的第一個(gè)參數(shù)中傳入this/self指針,而對(duì)于Go來(lái) 說(shuō),,receiver也是同樣道理,,將T的method轉(zhuǎn)換為普通function定義: func Get(t T) int { return t.a } 這種function形式被稱為Method Type,也可以稱為Method的signature,。 Method的一般使用方式如下: var t T 不過(guò)我們也可以像普通function那樣使用它,,根據(jù)上面的Method Type定義: var t T 這種以直接以類型名T調(diào)用方法M的表達(dá)方法稱為Method Expression。類型T只能調(diào)用T的Method Set中的方法,;同理*T只能調(diào)用*T的Method Set中的方法,。上述例子中T的Method Set中只有Get,因此T.Get是合法的,。但T.Set則不合法: T.Set(2) //invalid method expression T.Set (needs pointer receiver: (*T).Set) 我們只能使用(*T).Set(&t, 11),。 這樣看來(lái)Method Expression有些類似于C++中的static方法(以該類的某個(gè)對(duì)象實(shí)例作為第一個(gè)參數(shù))。 另外Method express自身類型就是一個(gè)普通function,,可以作為右值賦值給一個(gè)函數(shù)類型的變量: f1 := (*T).Set //函數(shù)類型:func (t *T, int)int Go中還定義了一種與Method有關(guān)的語(yǔ)法:如果一個(gè)表達(dá)式t具有靜態(tài)類型T,,M是T的Method Set中的一個(gè)方法,那么t.M即為Method Value。注意這里是t.M而不是T.M,。 f3 := (&t).Set //函數(shù)類型:func(int)int 可以看出,,Method value與Method Expression不同之處在于,Method value綁定了T對(duì)象實(shí)例,,它的函數(shù)原型并不包含Method Expression函數(shù)原型中的第一個(gè)參數(shù),。完整例子參見(jiàn):details-in-go/4/methodexpressionandmethodvalue.go。 五,、for range“坑”大閱兵 for range的引入提升了Go的表達(dá)能力,,但for range顯然不是”免費(fèi)的午餐“,在享用這個(gè)美味前,,需要搞清楚for range的一些坑,。 1、iteration variable重用 for range的idiomatic的使用方式是使用short variable declaration(:=)形式在for expression中聲明iteration variable,,但需要注意的是這些variable在每次循環(huán)體中都會(huì)被重用,而不是重新聲明,。 //details-in-go/5/iterationvariable.go for i, v := range m { time.Sleep(time.Second * 10) 在我的Mac上,,輸出結(jié)果如下: $go run iterationvariable.go 各個(gè)goroutine中輸出的i,v值都是for range循環(huán)結(jié)束后的i, v最終值,而不是各個(gè)goroutine啟動(dòng)時(shí)的i, v值,。一個(gè)可行的fix方法: for i, v := range m { 2,、range expression副本參與iteration range后面接受的表達(dá)式的類型包括:array, pointer to array, slice, string, map和channel(有讀權(quán)限的)。我們以array為例來(lái)看一個(gè)簡(jiǎn)單的例子: //details-in-go/5/arrayrangeexpression.go fmt.Println("a = ", a) for i, v := range a { fmt.Println("r = ", r) 我們期待輸出結(jié)果: a = [1 2 3 4 5] 但實(shí)際輸出結(jié)果卻是: a = [1 2 3 4 5] 我們?cè)詾樵诘谝淮蝘teration,,也就是i = 0時(shí),,我們對(duì)a的修改(a[1] = 12,a[2] = 13)會(huì)在第二次,、第三次循環(huán)中被v取出,,但結(jié)果卻是v取出的依舊是a被修改前的值:2和3。這就是for range的一個(gè)不大不小的坑:range expression副本參與循環(huán),。也就是說(shuō)在上面這個(gè)例子里,,真正參與循環(huán)的是a的副本,而不是真正的a,,偽代碼如 下: for i, v := range a' {//a' is copy from a Go中的數(shù)組在內(nèi)部表示為連續(xù)的字節(jié)序列,,雖然長(zhǎng)度是Go數(shù)組類型的一部分,但長(zhǎng)度并不包含的數(shù)組的內(nèi)部表示中,,而是由編譯器在編譯期計(jì)算出 來(lái),。這個(gè)例子中,對(duì)range表達(dá)式的拷貝,,即對(duì)一個(gè)數(shù)組的拷貝,,a'則是Go臨時(shí)分配的連續(xù)字節(jié)序列,與a完全不是一塊內(nèi)存。因此無(wú)論a被 如何修改,,其副本a'依舊保持原值,,并且參與循環(huán)的是a',因此v從a'中取出的仍舊是a的原值,,而非修改后的值,。 我們?cè)賮?lái)試試pointer to array: func pointerToArrayRangeExpression() { fmt.Println("pointerToArrayRangeExpression result:") for i, v := range &a { r[i] = v fmt.Println("r = ", r) 這回的輸出結(jié)果如下: pointerToArrayRangeExpression result: 我們看到這次r數(shù)組的值與最終a被修改后的值一致了。這個(gè)例子中我們使用了*[5]int作為range表達(dá)式,,其副本依舊是一個(gè)指向原數(shù)組 a的指針,,因此后續(xù)所有循環(huán)中均是&a指向的原數(shù)組親自參與的,因此v能從&a指向的原數(shù)組中取出a修改后的值,。 idiomatic go建議我們盡可能的用slice替換掉array的使用,,這里用slice能否實(shí)現(xiàn)預(yù)期的目標(biāo)呢?我們來(lái)試試: func sliceRangeExpression() { fmt.Println("sliceRangeExpression result:") for i, v := range a[:] { r[i] = v fmt.Println("r = ", r) pointerToArrayRangeExpression result: 顯然用slice也能實(shí)現(xiàn)預(yù)期要求,。我們可以分析一下slice是如何做到的,。slice在go的內(nèi)部表示為一個(gè)struct,由(*T, len, cap)組成,,其中*T指向slice對(duì)應(yīng)的underlying array的指針,,len是slice當(dāng)前長(zhǎng)度,cap為slice的最大容量,。當(dāng)range進(jìn)行expression復(fù)制時(shí),,它實(shí)際上復(fù)制的是一個(gè) slice,也就是那個(gè)struct,。副本struct中的*T依舊指向原slice對(duì)應(yīng)的array,,為此對(duì)slice的修改都反映到 underlying array a上去了,v從副本struct中*T指向的underlying array中獲取數(shù)組元素,,也就得到了被修改后的元素值,。 slice與array還有一個(gè)不同點(diǎn),就是其len在運(yùn)行時(shí)可以被改變,,而array的len是一個(gè)常量,,不可改變。那么len變化的 slice對(duì)for range有何影響呢,?我們繼續(xù)看一個(gè)例子: func sliceLenChangeRangeExpression() { fmt.Println("sliceLenChangeRangeExpression result:") for i, v := range a { r = append(r, v) fmt.Println("r = ", r) 輸出結(jié)果: a = [1 2 3 4 5] 在這個(gè)例子中,,原slice a在for range過(guò)程中被附加了兩個(gè)元素6和7,其len由5增加到7,,但這對(duì)于r卻沒(méi)有產(chǎn)生影響,。這里的原因就在于a的副本a'的內(nèi)部表示struct中的 len字段并沒(méi)有改變,依舊是5,,因此for range只會(huì)循環(huán)5次,,也就只獲取a對(duì)應(yīng)的underlying數(shù)組的前5個(gè)元素,。 range的副本行為會(huì)帶來(lái)一些性能上的消耗,尤其是當(dāng)range expression的類型為數(shù)組時(shí),,range需要復(fù)制整個(gè)數(shù)組,;而當(dāng)range expression類型為pointer to array或slice時(shí),這個(gè)消耗將小得多,,僅僅需要復(fù)制一個(gè)指針或一個(gè)slice的內(nèi)部表示(一個(gè)struct)即可,。我們可以通過(guò) benchmark test來(lái)看一下三種情況的消耗情況對(duì)比: 對(duì)于元素個(gè)數(shù)為100的int數(shù)組或slice,測(cè)試結(jié)果如下: //details-in-go/5/arraybenchmark 可以看到range expression類型為slice或pointer to array的性能相近,,消耗都近乎是數(shù)組類型的1/2,。 3、其他range expression類型 對(duì)于range后面的其他表達(dá)式類型,,比如string, map, channel,,for range依舊會(huì)制作副本。 【string】 var s = "中國(guó)人" for i, v := range s { 輸出結(jié)果: 如果s中存在非法utf8字節(jié)序列,,那么v將返回0xFFFD這個(gè)特殊值,,并且在接下來(lái)一輪循環(huán)中,,v將僅前進(jìn)一個(gè)字節(jié): //byte sequence of s: 0xe4 0xb8 0xad 0xe5 0x9b 0xbd 0xe4 0xba 0xba sl[3] = 0xd0 for i, v := range string(sl) { 輸出結(jié)果: 0xe4 0xb8 0xad 0xe5 0x9b 0xbd 0xe4 0xba 0xba 0 4e2d 以上例子源碼在details-in-go/5/stringrangeexpression.go中可以找到。 【map】 對(duì)于map來(lái)說(shuō),,map內(nèi)部表示為一個(gè)指針,,指針副本也指向真實(shí)map,因此for range操作均操作的是源map,。 for range不保證每次迭代的元素次序,,對(duì)于下面代碼: var m = map[string]int{ for k, v := range m { 輸出結(jié)果可能是: tom 22 也可能是: tony 21 或其他可能。 如果map中的某項(xiàng)在循環(huán)到達(dá)前被在循環(huán)體中刪除了,,那么它將不會(huì)被iteration variable獲取到,。 反復(fù)運(yùn)行多次,我們得到的兩個(gè)結(jié)果: tony 21 tom 22 如果在循環(huán)體中新創(chuàng)建一個(gè)map元素項(xiàng),,那該項(xiàng)元素可能出現(xiàn)在后續(xù)循環(huán)中,,也可能不出現(xiàn): m["tony"] = 21 for k, v := range m { 執(zhí)行結(jié)果: tony 21 or tony 21 以上代碼可以在details-in-go/5/maprangeexpression.go中可以找到。 【channel】 對(duì)于channel來(lái)說(shuō),,channel內(nèi)部表示為一個(gè)指針,,channel的指針副本也指向真實(shí)channel,。 for range最終以阻塞讀的方式阻塞在channel expression上(即便是buffered channel,當(dāng)channel中無(wú)數(shù)據(jù)時(shí),,for range也會(huì)阻塞在channel上),,直到channel關(guān)閉: //details-in-go/5/channelrangeexpression.go go func() { for v := range c { 運(yùn)行結(jié)果: 1 如果channel變量為nil,則for range將永遠(yuǎn)阻塞,。 六,、select求值 golang引入的select為我們提供了一種在多個(gè)channel間實(shí)現(xiàn)“多路復(fù)用”的一種機(jī)制。select的運(yùn)行機(jī)制這里不贅述,,但select的case expression的求值順序我們倒是要通過(guò)一個(gè)例子來(lái)了解一下: // details-in-go/6/select.go func takeARecvChannel() chan int { go func() { return c func getAStorageArr() *[5]int { func takeASendChannel() chan int { func getANumToChannel() int { func main() { //send channels 運(yùn)行結(jié)果: $go run select.go invoke getAStorageArr 通過(guò)例子我們可以看出: invoke takeARecvChannel 例外的是recv channel的位于賦值等號(hào)左邊的表達(dá)式(這里是:(getAStorageArr())[0])不會(huì)被求值,。 2) 如果選擇要執(zhí)行的case是一個(gè)recv channel,那么它的賦值等號(hào)左邊的表達(dá)式會(huì)被求值:如例子中當(dāng)goroutine 3s后向recvchan寫入一個(gè)int值后,,select選擇了recv channel執(zhí)行,,此時(shí)對(duì)=左側(cè)的表達(dá)式 (getAStorageArr())[0] 開(kāi)始求值,輸出“invoke getAStorageArr”,。 七,、panic的recover過(guò)程 Go沒(méi)有提供“try-catch-finally”這樣的異常處理設(shè)施,而僅僅提供了panic和recover,,其中recover還要結(jié)合 defer使用,。最初這也是被一些人詬病的點(diǎn)。但和錯(cuò)誤碼返回值一樣,,漸漸的大家似乎適應(yīng)了這些,,征討之聲漸稀,即便有也是排在“缺少generics” 之后了,。 【panicking】 在沒(méi)有recover的時(shí)候,,一旦panic發(fā)生,panic會(huì)按既定順序結(jié)束當(dāng)前進(jìn)程,,這一過(guò)程成為panicking,。下面的例子模擬了這一過(guò)程: //details-in-go/7/panicking.go bar() func bar() { zoo() func zoo() { fmt.Println("zoo invoked") func main() { 執(zhí)行結(jié)果: $go run panicking.go goroutine 1 [running]: 從結(jié)果可以看出: recover只有在defer函數(shù)中調(diào)用才能起到recover的作用,,這樣recover就和defer函數(shù)有了緊密聯(lián)系,。我們?cè)趜oo的defer函數(shù)中捕捉并recover這個(gè)panic: //details-in-go/7/recover.go defer func() { defer func() { fmt.Println("zoo invoked") 這回的執(zhí)行結(jié)果如下: $go run recover.go 由于zoo在defer里恢復(fù)了panic,這樣在zoo返回后,,bar不會(huì)感知到任何異常,,將按正常邏輯輸出函數(shù)執(zhí)行內(nèi)容,比如:“do something after zoo in bar”,以此類推,。 但若如果在zoo defer func中recover panic后,,又raise another panic,那么zoo對(duì)于bar來(lái)說(shuō)就又會(huì)變成panic了,。 Last,、參考資料 1、The Go Programming Language Specification (Version of August 5, 2015,,Go 1.5),; 本文實(shí)驗(yàn)環(huán)境:Go 1.5 darwin_amd64,。示例代碼在這里可以下載。 我就是這樣一種人:對(duì)任何自己感興趣且有極大熱情去做的事情都喜歡刨根問(wèn)底,,徹底全面地了解其中細(xì)節(jié),,否則我就會(huì)有一種“不安全 感”。我不知道在心理學(xué)范疇這樣的我屬于那種類別^_^,。 2015, bigwhite. 版權(quán)所有. Related posts: |
|