這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
年後事情實在是多,各種被催進度,於是好長一段時間未更部落格了,自責中….。今天蹦出來熱熱身^0^!
中午在微博私信中看到一封來自某Gopher的諮詢,他貼了一段代碼,並表示對代碼的輸出結果的不解,希望我能幫他分析一下。他的代碼如下:
//testslicerange.gopackage mainimport ( "fmt" "time")type field struct { name string}func (p *field) print() { fmt.Println(p.name)}func main() { data1 := []*field{{"one"}, {"two"}, {"three"}} for _, v := range data1 { go v.print() } data2 := []field{{"four"}, {"five"}, {"six"}} for _, v := range data2 { go v.print() } time.Sleep(3 * time.Second)}
在go playground上,其輸出結果為(在我的多核mac,Go 1.10上面程式與此稍有不同,輸出的item相同,只是前後順序有不同):
onetwothreesixsixsix
雖然這位Gopher並沒有明確說明他的疑惑究竟是什嗎?但從上述的輸出結果來看,他一定是想問:為什麼對data2的迭代輸出的是三個”six”,而不是four、five、six?
好了,我來分析一下。首先,我要對這個程式做個等價變換,變換後的程式源碼如下:
//testslicerange-transform.gopackage mainimport ( "fmt" "time")type field struct { name string}func print(p *field) { fmt.Println(p.name)}func main() { data1 := []*field{{"one"}, {"two"}, {"three"}} for _, v := range data1 { go print(v) } data2 := []field{{"four"}, {"five"}, {"six"}} for _, v := range data2 { go print(&v) } time.Sleep(3 * time.Second)}
這裡我把field結構體的method:print,換成了普通的以field指標作為第一個參數的函數print,這個變換是等價的,因為go中的method本質上就是以method的receiver作為第一個參數的普通function,即:
instance.method(x,y) <=> function(instance, x,y)
因此,執行上述的變換後的testslicerange-transform.go,得到的結果與testslicerange.go是一致的:
onetwothreesixsixsix
這樣變換以後,問題是不是豁然開朗了,你可以很清楚地看到使用go關鍵字啟動一個新goroutine時是如何綁定參數的:
- 迭代data1時,由於data1中的元素類型是field指標,因此賦值後v就是元素地址, 每次調用print時傳入的參數(v)實際上也是各個field元素的地址;- 迭代data2時,由於data2中的元素類型是field(非指標),因此賦值後v是元素的copy,每次傳入的&v實際上是v的地址,而不是被copy的元素的地址;
剩下的就是for range常見的那個”坑”的問題(在我的《關於Go,你可能不注意的7件事》一文中有詳盡說明),那就是v在整個for range過程只有一個,data2迭代完成之後,v是元素”six”的copy。
這樣,一旦啟動的各個child goroutine在main goroutine執行到Sleep時才被調度執行,那麼最後的三個goroutine在列印&v時,列印的也就都v
中存放的值”six”了。而前三個child goroutine各自傳入的是元素(“one”、”two”、”three”)的地址,列印的就是”one”、”two”、”three”。
那麼原程式如何修改一下才能讓其按期望輸出(“one”、”two”、”three”, “four”, “five”, “six”)呢?我們來改一下:只需將field method的receiver type由*field改為field即可。
// testslicerange1.gopackage mainimport ( "fmt" "time")type field struct { name string}func (p field) print() { fmt.Println(p.name)}func main() { data1 := []*field{{"one"}, {"two"}, {"three"}} for _, v := range data1 { go v.print() } data2 := []field{{"four"}, {"five"}, {"six"}} for _, v := range data2 { go v.print() } time.Sleep(3 * time.Second)}
上述程式在go playground上的輸出為:
onetwothreefourfivesix
至於為什麼,可以參考我的分析思路,自行分析一下。
著名雲主機服務廠商DigitalOcean發布最新的主機計劃,入門級Droplet配置升級為:1 core CPU、1G記憶體、25G高速SSD,價格5$/月。有使用DigitalOcean需求的朋友,可以開啟這個連結地址:https://m.do.co/c/bff6eed92687 開啟你的DO主機之路。
我的連絡方式:
微博:https://weibo.com/bigwhite20xx
公眾號:iamtonybai
部落格:tonybai.com
github: https://github.com/bigwhite
讚賞:
商務合作方式:撰稿、出書、培訓、線上課程、合夥創業、諮詢、廣告合作。
2018, bigwhite. 著作權.
Related posts:
- 關於Go,你可能不注意的7件事
- Go與C語言的互操作
- Go 1.4中值得關注的幾個變化
- 近期遇到的3個Golang代碼問題
- Go中的系統Signal處理