Go是一門簡單有趣的語言,但與其他語言類似,它會有一些技巧。。。這些技巧的絕大部分并不是Go的缺陷造成的。如果你以前使用的是其他語言,那么這其中的有些錯誤就是很自然的陷阱。其它的是由錯誤的假設和缺少細節造成的。
如果你花時間學習這門語言,閱讀官方說明、wiki、郵件列表討論、大量的優秀博文和Rob Pike的展示,以及源代碼,這些技巧中的絕大多數都是顯而易見的。盡管不是每個人都是以這種方式開始學習的,但也沒關系。如果你是Go語言新人,那么這里的信息將會節約你大量的調試代碼的時間。
目錄
初級篇
開大括號不能放在單獨的一行
未使用的變量
未使用的Imports
簡式的變量聲明僅可以在函數內部使用
使用簡式聲明重復聲明變量
偶然的變量隱藏Accidental Variable Shadowing
不使用顯式類型,無法使用“nil”來初始化變量
使用“nil” Slices and Maps
Map的容量
字符串不會為“nil”
Array函數的參數
在Slice和Array使用“range”語句時的出現的不希望得到的值
Slices和Arrays是一維的
訪問不存在的Map Keys
Strings無法修改
String和Byte Slice之間的轉換
String和索引操作
字符串不總是UTF8文本
字符串的長度
在多行的Slice、Array和Map語句中遺漏逗號
log.Fatal和log.Panic不僅僅是Log
內建的數據結構操作不是同步的
String在“range”語句中的迭代值
對Map使用“for range”語句迭代
"switch"聲明中的失效行為
自增和自減
按位NOT操作
操作優先級的差異
未導出的結構體不會被編碼
有活動的Goroutines下的應用退出
向無緩存的Channel發送消息,只要目標接收者準備好就會立即返回
向已關閉的Channel發送會引起Panic
使用"nil" Channels
傳值方法的接收者無法修改原有的值
進階篇
關閉HTTP的響應
關閉HTTP的連接
比較Structs, Arrays, Slices, and Maps
從Panic中恢復
在Slice, Array, and Map "range"語句中更新引用元素的值
在Slice中"隱藏"數據
Slice的數據“毀壞”
"走味的"Slices
類型聲明和方法
從"for switch"和"for select"代碼塊中跳出
"for"聲明中的迭代變量和閉包
Defer函數調用參數的求值
被Defer的函數調用執行
失敗的類型斷言
阻塞的Goroutine和資源泄露
高級篇
使用指針接收方法的值的實例
更新Map的值
"nil" Interfaces和"nil" Interfaces的值
棧和堆變量
GOMAXPROCS, 并發, 和并行
讀寫操作的重排順序
優先調度
初級篇
開大括號不能放在單獨的一行
- level: beginner
在大多數其他使用大括號的語言中,你需要選擇放置它們的位置。Go的方式不同。你可以為此感謝下自動分號的注入(沒有預讀)。是的,Go中也是有分號的:-)
失敗的例子:
package main
import"fmt"
func main(){//error, can't have the opening brace on a separate line
fmt.Println("hello there!")}
編譯錯誤:
/tmp/sandbox826898458/main.go:6: syntax error: unexpected semicolon or newline before {
有效的例子:
package main
import"fmt"
func main(){
fmt.Println("works!")}
未使用的變量
- level: beginner
如果你有未使用的變量,代碼將編譯失敗。當然也有例外。在函數內一定要使用聲明的變量,但未使用的全局變量是沒問題的。
如果你給未使用的變量分配了一個新的值,代碼還是會編譯失敗。你需要在某個地方使用這個變量,才能讓編譯器愉快的編譯。
Fails:
package main
var gvar int//not an error
func main(){var one int//error, unused variable
two :=2//error, unused variablevar three int//error, even though it's assigned 3 on the next line
three =3}
Compile Errors:
/tmp/sandbox473116179/main.go:6: one declared andnot used /tmp/sandbox473116179/main.go:7: two declared andnot used /tmp/sandbox473116179/main.go:8: three declared andnot used
Works:
package main
import"fmt"
func main(){var one int
_ = one
two :=2
fmt.Println(two)var three int
three =3
one = three
var four int
four = four
}
另一個選擇是注釋掉或者移除未使用的變量 :-)
未使用的Imports
- level: beginner
如果你引入一個包,而沒有使用其中的任何函數、接口、結構體或者變量的話,代碼將會編譯失敗。
如果你真的需要引入的包,你可以添加一個下劃線標記符, _
,來作為這個包的名字,從而避免編譯失敗。下滑線標記符用于引入,但不使用。
Fails:
package main
import("fmt""log""time")
func main(){}
Compile Errors:
/tmp/sandbox627475386/main.go:4: imported andnot used:"fmt"/tmp/sandbox627475386/main.go:5: imported andnot used:"log"/tmp/sandbox627475386/main.go:6: imported andnot used:"time"
Works:
package main
import(
_ "fmt""log""time")var _ = log.Println
func main(){
_ = time.Now}
另一個選擇是移除或者注釋掉未使用的imports :-)
簡式的變量聲明僅可以在函數內部使用
- level: beginner
Fails:
package main
myvar :=1//error
func main(){}
Compile Error:
/tmp/sandbox265716165/main.go:3: non-declaration statement outside function body
Works:
package main
var myvar =1
func main(){}
使用簡式聲明重復聲明變量
- level: beginner
你不能在一個單獨的聲明中重復聲明一個變量,但在多變量聲明中這是允許的,其中至少要有一個新的聲明變量。
重復變量需要在相同的代碼塊內,否則你將得到一個隱藏變量。
Fails:
package main
func main(){
one :=0
one :=1//error}
Compile Error:
/tmp/sandbox706333626/main.go:5:nonew variables on left side of :=
Works:
package main
func main(){
one :=0
one, two :=1,2
one,two = two,one
}
偶然的變量隱藏Accidental Variable Shadowing
- level: beginner
短式變量聲明的語法如此的方便(尤其對于那些使用過動態語言的開發者而言),很容易讓人把它當成一個正常的分配操作。如果你在一個新的代碼塊中犯了這個錯誤,將不會出現編譯錯誤,但你的應用將不會做你所期望的事情。
package main
import"fmt"
func main(){
x :=1
fmt.Println(x)//prints 1{
fmt.Println(x)//prints 1
x :=2
fmt.Println(x)//prints 2}
fmt.Println(x)//prints 1 (bad if you need 2)}
即使對于經驗豐富的Go開發者而言,這也是一個非常常見的陷阱。這個坑很容易挖,但又很難發現。
不使用顯式類型,無法使用“nil”來初始化變量
- level: beginner
“nil”標志符用于表示interface、函數、maps、slices和channels的“零值”。如果你不指定變量的類型,編譯器將無法編譯你的代碼,因為它猜不出具體的類型。
Fails:
package main
func main(){var x =nil//error
_ = x
}
Compile Error:
/tmp/sandbox188239583/main.go:4:use of untyped nil
Works:
package main
func main(){var x interface{}=nil
_ = x
}
使用“nil” Slices and Maps
- level: beginner
在一個“nil”的slice中添加元素是沒問題的,但對一個map做同樣的事將會生成一個運行時的panic。
Works:
package main
func main(){var s []int
s = append(s,1)}
Fails:
package main
func main(){var m map[string]int
m["one"]=1//error}
Map的容量
- level: beginner
你可以在map創建時指定它的容量,但你無法在map上使用cap()函數。
Fails:
package main
func main(){
m := make(map[string]int,99)
cap(m)//error}
Compile Error:
/tmp/sandbox326543983/main.go:5: invalid argument m (type map[string]int)for cap
字符串不會為“nil”
- level: beginner
這對于經常使用“nil”分配字符串變量的開發者而言是個需要注意的地方。
Fails:
package main
func main(){var x string=nil//errorif x ==nil{//error
x ="default"}}
Compile Errors:
/tmp/sandbox630560459/main.go:4: cannot usenilas type stringin assignment /tmp/sandbox630560459/main.go:6: invalid operation: x ==nil(mismatched types stringandnil)
Works:
package main
func main(){var x string//defaults to "" (zero value)if x ==""{
x ="default"}}
Array函數的參數
-level: beginner
如果你是一個C或則C++開發者,那么數組對你而言就是指針。當你向函數中傳遞數組時,函數會參照相同的內存區域,這樣它們就可以修改原始的數據。Go中的數組是數值,因此當你向函數中傳遞數組時,函數會得到原始數組數據的一份復制。如果你打算更新數組的數據,這將會是個問題。
package main
import"fmt"
func main(){
x :=[3]int{1,2,3}
func(arr [3]int){
arr[0]=7
fmt.Println(arr)//prints [7 2 3]}(x)
fmt.Println(x)//prints [1 2 3] (not ok if you need [7 2 3])}
如果你需要更新原始數組的數據,你可以使用數組指針類型。
package main
import"fmt"
func main(){
x :=[3]int{1,2,3}
func(arr *[3]int){(*arr)[0]=7
fmt.Println(arr)//prints &[7 2 3]}(&x)
fmt.Println(x)//prints [7 2 3]}
另一個選擇是使用slice。即使你的函數得到了slice變量的一份拷貝,它依舊會參照原始的數據。
package main
import"fmt"
func main(){
x :=[]int{1,2,3}
func(arr []int){
arr[0]=7
fmt.Println(arr)//prints [7 2 3]}(x)
fmt.Println(x)//prints [7 2 3]}
在Slice和Array使用“range”語句時的出現的不希望得到的值
- level: beginner
如果你在其他的語言中使用“for-in”或者“foreach”語句時會發生這種情況。Go中的“range”語法不太一樣。它會得到兩個值:第一個值是元素的索引,而另一個值是元素的數據。
Bad:
package main
import"fmt"
func main(){
x :=[]string{"a","b","c"}for v := range x {
fmt.Println(v)//prints 0, 1, 2}}
Good:
package main
import"fmt"
func main(){
x :=[]string{"a","b","c"}for _, v := range x {
fmt.Println(v)//prints a, b, c}}
Slices和Arrays是一維的
- level: beginner
看起來Go好像支持多維的Array和Slice,但不是這樣的。盡管可以創建數組的數組或者切片的切片。對于依賴于動態多維數組的數值計算應用而言,Go在性能和復雜度上還相距甚遠。
你可以使用純一維數組、“獨立”切片的切片,“共享數據”切片的切片來構建動態的多維數組。
如果你使用純一維的數組,你需要處理索引、邊界檢查、當數組需要變大時的內存重新分配。
使用“獨立”slice來創建一個動態的多維數組需要兩步。首先,你需要創建一個外部的slice。然后,你需要分配每個內部的slice。內部的slice相互之間獨立。你可以增加減少它們,而不會影響其他內部的slice。
package main
func main(){
x :=2
y :=4
table := make([][]int,x)for i:= range table {
table[i]= make([]int,y)}}
使用“共享數據”slice的slice來創建一個動態的多維數組需要三步。首先,你需要創建一個用于存放原始數據的數據“容器”。然后,你再創建外部的slice。最后,通過重新切片原始數據slice來初始化各個內部的slice。
package main
import"fmt"
func main(){
h, w :=2,4
raw := make([]int,h*w)for i := range raw {
raw[i]= i
}
fmt.Println(raw,&raw[4])//prints: [0 1 2 3 4 5 6 7] <ptr_addr_x>
table := make([][]int,h)for i:= range table {
table[i]= raw[i*w:i*w + w]}
fmt.Println(table,&table[1][0])//prints: [[0 1 2 3] [4 5 6 7]] <ptr_addr_x>}
關于多維array和slice已經有了專門申請,但現在看起來這是個低優先級的特性。
訪問不存在的Map Keys
-level: beginner
這對于那些希望得到“nil”標示符的開發者而言是個技巧(和其他語言中做的一樣)。如果對應的數據類型的“零值”是“nil”,那返回的值將會是“nil”,但對于其他的數據類型是不一樣的。檢測對應的“零值”可以用于確定map中的記錄是否存在,但這并不總是可信(比如,如果在二值的map中“零值”是false,這時你要怎么做)。檢測給定map中的記錄是否存在的最可信的方法是,通過map的訪問操作,檢查第二個返回的值。
Bad:
package main
import"fmt"
func main(){
x := map[string]string{"one":"a","two":"","three":"c"}if v := x["two"]; v ==""{//incorrect
fmt.Println("no entry")}}
Good:
package main
import"fmt"
func main(){
x := map[string]string{"one":"a","two":"","three":"c"}if _,ok := x["two"];!ok {
fmt.Println("no entry")}}
Strings無法修改
- level: beginner
嘗試使用索引操作來更新字符串變量中的單個字符將會失敗。string是只讀的byte slice(和一些額外的屬性)。如果你確實需要更新一個字符串,那么使用byte slice,并在需要時把它轉換為string類型。
Fails:
package main
import"fmt"
func main(){
x :="text"
x[0]='T'
fmt.Println(x)}
Compile Error:
/tmp/sandbox305565531/main.go:7: cannot assign to x[0]
Works:
package main
import"fmt"
func main(){
x :="text"
xbytes :=[]byte(x)
xbytes[0]='T'
fmt.Println(string(xbytes))//prints Text}
需要注意的是:這并不是在文字string中更新字符的正確方式,因為給定的字符可能會存儲在多個byte中。如果你確實需要更新一個文字string,先把它轉換為一個rune slice。即使使用rune slice,單個字符也可能會占據多個rune,比如當你的字符有特定的重音符號時就是這種情況。這種復雜又模糊的“字符”本質是Go字符串使用byte序列表示的原因。
String和Byte Slice之間的轉換
- level: beginner
當你把一個字符串轉換為一個byte slice(或者反之)時,你就得到了一個原始數據的完整拷貝。這和其他語言中cast操作不同,也和新的slice變量指向原始byte slice使用的相同數組時的重新slice操作不同。
Go在 []byte
到 string
和 string
到 []byte
的轉換中確實使用了一些優化來避免額外的分配(在todo列表中有更多的優化)。
第一個優化避免了當 []byte
key用于在 map[string]
集合中查詢時的額外分配: m[string(key)]
。
第二個優化避免了字符串轉換為 []byte
后在 for range
語句中的額外分配: for i,v := range []byte(str){...}
。
String和索引操作
- level: beginner
字符串上的索引操作返回一個byte值,而不是一個字符(和其他語言中的做法一樣)。
package main
import"fmt"
func main(){
x :="text"
fmt.Println(x[0])//print 116
fmt.Printf("%T",x[0])//prints uint8}
如果你需要訪問特定的字符串“字符”(unicode編碼的points/runes),使用 for range
。官方的“unicode/utf8”包和實驗中的utf8string包(golang.org/x/exp/utf8string)也可以用。utf8string包中包含了一個很方便的 At()
方法。把字符串轉換為rune的切片也是一個選項。
字符串不總是UTF8文本
- level: beginner
字符串的值不需要是UTF8的文本。它們可以包含任意的字節。只有在string literal使用時,字符串才會是UTF8。即使之后它們可以使用轉義序列來包含其他的數據。
為了知道字符串是否是UTF8,你可以使用“unicode/utf8”包中的 ValidString()
函數。
package main
import("fmt""unicode/utf8")
func main(){
data1 :="ABC"
fmt.Println(utf8.ValidString(data1))//prints: true
data2 :="A\xfeC"
fmt.Println(utf8.ValidString(data2))//prints: false}
字符串的長度
- level: beginner
讓我們假設你是Python開發者,你有下面這段代碼:
data = u'?'print(len(data))#prints: 1
當把它轉換為Go代碼時,你可能會大吃一驚。
package main
import"fmt"
func main(){
data :="?"
fmt.Println(len(data))//prints: 3}
內建的 len()
函數返回byte的數量,而不是像Python中計算好的unicode字符串中字符的數量。
要在Go中得到相同的結果,可以使用“unicode/utf8”包中的 RuneCountInString()
函數。
package main
import("fmt""unicode/utf8")
func main(){
data :="?"
fmt.Println(utf8.RuneCountInString(data))//prints: 1}
理論上說 RuneCountInString()
函數并不返回字符的數量,因為單個字符可能占用多個rune。
package main
import("fmt""unicode/utf8")
func main(){
data :="e?"
fmt.Println(len(data))//prints: 3
fmt.Println(utf8.RuneCountInString(data))//prints: 2}
在多行的Slice、Array和Map語句中遺漏逗號
- level: beginner
Fails:
package main
func main(){
x :=[]int{1,2//error}
_ = x
}
Compile Errors:
/tmp/sandbox367520156/main.go:6: syntax error: need trailing comma before newline in composite literal /tmp/sandbox367520156/main.go:8: non-declaration statement outside function body /tmp/sandbox367520156/main.go:9: syntax error: unexpected }
Works:
package main
func main(){
x :=[]int{1,2,}
x = x
y :=[]int{3,4,}//no error
y = y
}
當你把聲明折疊到單行時,如果你沒加末尾的逗號,你將不會得到編譯錯誤。
log.Fatal和log.Panic不僅僅是Log
- level: beginner
Logging庫一般提供不同的log等級。與這些logging庫不同,Go中log包在你調用它的 Fatal*()
和 Panic*()
函數時,可以做的不僅僅是log。當你的應用調用這些函數時,Go也將會終止應用 :-)
package main
import"log"
func main(){
log.Fatalln("Fatal Level: log entry")//app exits here
log.Println("Normal Level: log entry")}
內建的數據結構操作不是同步的
- level: beginner
即使Go本身有很多特性來支持并發,并發安全的數據集合并不是其中之一 :-)確保數據集合以原子的方式更新是你的職責。Goroutines和channels是實現這些原子操作的推薦方式,但你也可以使用“sync”包,如果它對你的應用有意義的話。
String在“range”語句中的迭代值
- level: beginner
索引值(“range”操作返回的第一個值)是返回的第二個值的當前“字符”(unicode編碼的point/rune)的第一個byte的索引。它不是當前“字符”的索引,這與其他語言不同。注意真實的字符可能會由多個rune表示。如果你需要處理字符,確保你使用了“norm”包(golang.org/x/text/unicode/norm)。
string變量的 for range
語句將會嘗試把數據翻譯為UTF8文本。對于它無法理解的任何byte序列,它將返回0xfffd runes(即unicode替換字符),而不是真實的數據。如果你任意(非UTF8文本)的數據保存在string變量中,確保把它們轉換為byte slice,以得到所有保存的數據。
package main
import"fmt"
func main(){
data :="A\xfe\x02\xff\x04"for _,v := range data {
fmt.Printf("%#x ",v)}//prints: 0x41 0xfffd 0x2 0xfffd 0x4 (not ok)
fmt.Println()for _,v := range []byte(data){
fmt.Printf("%#x ",v)}//prints: 0x41 0xfe 0x2 0xff 0x4 (good)}
對Map使用“for range”語句迭代
- level: beginner
如果你希望以某個順序(比如,按key值排序)的方式得到元素,就需要這個技巧。每次的map迭代將會生成不同的結果。Go的runtime有心嘗試隨機化迭代順序,但并不總會成功,這樣你可能得到一些相同的map迭代結果。所以如果連續看到5個相同的迭代結果,不要驚訝。
package main
import"fmt"
func main(){
m := map[string]int{"one":1,"two":2,"three":3,"four":4}for k,v := range m {
fmt.Println(k,v)}}
而且如果你使用Go的游樂場(https://play.golang.org/),你將總會得到同樣的結果,因為除非你修改代碼,否則它不會重新編譯代碼。
"switch"聲明中的失效行為
- level: beginner
在“switch”聲明語句中的“case”語句塊在默認情況下會break。這和其他語言中的進入下一個“next”代碼塊的默認行為不同。
package main
import"fmt"
func main(){
isSpace := func(ch byte)bool{switch(ch){case' '://errorcase'\t':returntrue}returnfalse}
fmt.Println(isSpace('\t'))//prints true (ok)
fmt.Println(isSpace(' '))//prints false (not ok)}
你可以通過在每個“case”塊的結尾使用“fallthrough”,來強制“case”代碼塊進入。你也可以重寫switch語句,來使用“case”塊中的表達式列表。
package main
import"fmt"
func main(){
isSpace := func(ch byte)bool{switch(ch){case' ','\t':returntrue}returnfalse}
fmt.Println(isSpace('\t'))//prints true (ok)
fmt.Println(isSpace(' '))//prints true (ok)}
自增和自減
- level: beginner
許多語言都有自增和自減操作。不像其他語言,Go不支持前置版本的操作。你也無法在表達式中使用這兩個操作符。
Fails:
package main
import"fmt"
func main(){
data :=[]int{1,2,3}
i :=0++i //error
fmt.Println(data[i++])//error}
Compile Errors:
/tmp/sandbox101231828/main.go:8: syntax error: unexpected ++/tmp/sandbox101231828/main.go:9: syntax error: unexpected ++, expecting :
Works:
package main
import"fmt"
func main(){
data :=[]int{1,2,3}
i :=0
i++
fmt.Println(data[i])}
按位NOT操作
- level: beginner
許多語言使用 ~
作為一元的NOT操作符(即按位補足),但Go為了這個重用了XOR操作符( ^
)。
Fails:
package main
import"fmt"
func main(){
fmt.Println(~2)//error}
Compile Error:
/tmp/sandbox965529189/main.go:6: the bitwise complement operatoris^
Works:
package main
import"fmt"
func main(){var d uint8 =2
fmt.Printf("%08b\n",^d)}
Go依舊使用 ^
作為XOR的操作符,這可能會讓一些人迷惑。
如果你愿意,你可以使用一個二元的XOR操作(如, 0x02 XOR 0xff)來表示一個一元的NOT操作(如,NOT 0x02)。這可以解釋為什么 ^
被重用來表示一元的NOT操作。
Go也有特殊的‘AND NOT’按位操作( &^
),這也讓NOT操作更加的讓人迷惑。這看起來需要特殊的特性/hack來支持 A AND (NOT B)
,而無需括號。
package main
import"fmt"
func main(){var a uint8 =0x82var b uint8 =0x02
fmt.Printf("%08b [A]\n",a)
fmt.Printf("%08b [B]\n",b)
fmt.Printf("%08b (NOT B)\n",^b)
fmt.Printf("%08b ^ %08b = %08b [B XOR 0xff]\n",b,0xff,b ^0xff)
fmt.Printf("%08b ^ %08b = %08b [A XOR B]\n",a,b,a ^ b)
fmt.Printf("%08b & %08b = %08b [A AND B]\n",a,b,a & b)
fmt.Printf("%08b &^%08b = %08b [A 'AND NOT' B]\n",a,b,a &^ b)
fmt.Printf("%08b&(^%08b)= %08b [A AND (NOT B)]\n",a,b,a &(^b))}
操作優先級的差異
- level: beginner
除了”bit clear“操作( &^
),Go也一個與許多其他語言共享的標準操作符的集合。盡管操作優先級并不總是一樣。
package main
import"fmt"
func main(){
fmt.Printf("0x2 & 0x2 + 0x4 -> %#x\n",0x2&0x2+0x4)//prints: 0x2 & 0x2 + 0x4 -> 0x6//Go: (0x2 & 0x2) + 0x4//C++: 0x2 & (0x2 + 0x4) -> 0x2
fmt.Printf("0x2 + 0x2 << 0x1 -> %#x\n",0x2+0x2<<0x1)//prints: 0x2 + 0x2 << 0x1 -> 0x6//Go: 0x2 + (0x2 << 0x1)//C++: (0x2 + 0x2) << 0x1 -> 0x8
fmt.Printf("0xf | 0x2 ^ 0x2 -> %#x\n",0xf|0x2^0x2)//prints: 0xf | 0x2 ^ 0x2 -> 0xd//Go: (0xf | 0x2) ^ 0x2//C++: 0xf | (0x2 ^ 0x2) -> 0xf}
未導出的結構體不會被編碼
- level: beginner
以小寫字母開頭的結構體將不會被(json、xml、gob等)編碼,因此當你編碼這些未導出的結構體時,你將會得到零值。
Fails:
package main
import("fmt""encoding/json")
type MyDatastruct{Oneint
two string}
func main(){in:=MyData{1,"two"}
fmt.Printf("%#v\n",in)//prints main.MyData{One:1, two:"two"}
encoded,_ := json.Marshal(in)
fmt.Println(string(encoded))//prints {"One":1}varoutMyData
json.Unmarshal(encoded,&out)
fmt.Printf("%#v\n",out)//prints main.MyData{One:1, two:""}}
有活動的Goroutines下的應用退出
- level: beginner
應用將不會得帶所有的goroutines完成。這對于初學者而言是個很常見的錯誤。每個人都是以某個程度開始,因此如果犯了初學者的錯誤也沒神馬好丟臉的 :-)
package main
import("fmt""time")
func main(){
workerCount :=2for i :=0; i < workerCount; i++{
go doit(i)}
time.Sleep(1* time.Second)
fmt.Println("all done!")}
func doit(workerId int){
fmt.Printf("[%v] is running\n",workerId)
time.Sleep(3* time.Second)
fmt.Printf("[%v] is done\n",workerId)}
你將會看到:
[0]is running
[1]is running
all done!
一個最常見的解決方法是使用“WaitGroup”變量。它將會讓主goroutine等待所有的worker goroutine完成。如果你的應用有長時運行的消息處理循環的worker,你也將需要一個方法向這些goroutine發送信號,讓它們退出。你可以給各個worker發送一個“kill”消息。另一個選項是關閉一個所有worker都接收的channel。這是一次向所有goroutine發送信號的簡單方式。
package main
import("fmt""sync")
func main(){var wg sync.WaitGroupdone:= make(chan struct{})
workerCount :=2for i :=0; i < workerCount; i++{
wg.Add(1)
go doit(i,done,wg)}
close(done)
wg.Wait()
fmt.Println("all done!")}
func doit(workerId int,done<-chan struct{},wg sync.WaitGroup){
fmt.Printf("[%v] is running\n",workerId)
defer wg.Done()<-done
fmt.Printf("[%v] is done\n",workerId)}
如果你運行這個應用,你將會看到:
[0]is running
[0]isdone[1]is running
[1]isdone
看起來所有的worker在主goroutine退出前都完成了。棒!然而,你也將會看到這個:
fatal error: all goroutines are asleep - deadlock!
這可不太好 :-) 發送了神馬?為什么會出現死鎖?worker退出了,它們也執行了 wg.Done()
。應用應該沒問題啊。
死鎖發生是因為各個worker都得到了原始的“WaitGroup”變量的一個拷貝。當worker執行 wg.Done()
時,并沒有在主goroutine上的“WaitGroup”變量上生效。
package main
import("fmt""sync")
func main(){var wg sync.WaitGroupdone:= make(chan struct{})
wq := make(chan interface{})
workerCount :=2for i :=0; i < workerCount; i++{
wg.Add(1)
go doit(i,wq,done,&wg)}for i :=0; i < workerCount; i++{
wq <- i
}
close(done)
wg.Wait()
fmt.Println("all done!")}
func doit(workerId int, wq <-chan interface{},done<-chan struct{},wg *sync.WaitGroup){
fmt.Printf("[%v] is running\n",workerId)
defer wg.Done()for{select{case m :=<- wq:
fmt.Printf("[%v] m => %v\n",workerId,m)case<-done:
fmt.Printf("[%v] is done\n",workerId)return}}}
現在它會如預期般工作 :-)
向無緩存的Channel發送消息,只要目標接收者準備好就會立即返回
- level: beginner
發送者將不會被阻塞,除非消息正在被接收者處理。根據你運行代碼的機器的不同,接收者的goroutine可能會或者不會有足夠的時間,在發送者繼續執行前處理消息。
package main
import"fmt"
func main(){
ch := make(chan string)
go func(){for m := range ch {
fmt.Println("processed:",m)}}()
ch <-"cmd.1"
ch <-"cmd.2"http://won't be processed}
向已關閉的Channel發送會引起Panic
- level: beginner
從一個關閉的channel接收是安全的。在接收狀態下的 ok
的返回值將被設置為 false
,這意味著沒有數據被接收。如果你從一個有緩存的channel接收,你將會首先得到緩存的數據,一旦它為空,返回的 ok
值將變為 false
。
向關閉的channel中發送數據會引起panic。這個行為有文檔說明,但對于新的Go開發者的直覺不同,他們可能希望發送行為與接收行為很像。
package main
import("fmt""time")
func main(){
ch := make(chan int)for i :=0; i <3; i++{
go func(idx int){
ch <-(idx +1)*2}(i)}//get the first result
fmt.Println(<-ch)
close(ch)//not ok (you still have other senders)//do other work
time.Sleep(2* time.Second)}
根據不同的應用,修復方法也將不同。可能是很小的代碼修改,也可能需要修改應用的設計。無論是哪種方法,你都需要確保你的應用不會向關閉的channel中發送數據。
上面那個有bug的例子可以通過使用一個特殊的廢棄的channel來向剩余的worker發送不再需要它們的結果的信號來修復。
package main
import("fmt""time")
func main(){
ch := make(chan int)done:= make(chan struct{})for i :=0; i <3; i++{
go func(idx int){select{case ch <-(idx +1)*2: fmt.Println(idx,"sent result")case<-done: fmt.Println(idx,"exiting")}}(i)}//get first result
fmt.Println("result:",<-ch)
close(done)//do other work
time.Sleep(3* time.Second)}
使用"nil" Channels
- level: beginner
在一個 nil
的channel上發送和接收操作會被永久阻塞。這個行為有詳細的文檔解釋,但它對于新的Go開發者而言是個驚喜。
package main
import("fmt""time")
func main(){var ch chan intfor i :=0; i <3; i++{
go func(idx int){
ch <-(idx +1)*2}(i)}//get first result
fmt.Println("result:",<-ch)//do other work
time.Sleep(2* time.Second)}
如果運行代碼你將會看到一個runtime錯誤:
fatal error: all goroutines are asleep - deadlock!
這個行為可以在 select
聲明中用于動態開啟和關閉 case
代碼塊的方法。
package main
import"fmt"import"time"
func main(){
inch := make(chan int)
outch := make(chan int)
go func(){varin<- chan int= inch
varout chan <-intvar val intfor{select{caseout<- val:out=nilin= inch
case val =<-in:out= outch
in=nil}}}()
go func(){for r := range outch {
fmt.Println("result:",r)}}()
time.Sleep(0)
inch <-1
inch <-2
time.Sleep(3* time.Second)}
傳值方法的接收者無法修改原有的值
- level: beginner
方法的接收者就像常規的函數參數。如果聲明為值,那么你的函數/方法得到的是接收者參數的拷貝。這意味著對接收者所做的修改將不會影響原有的值,除非接收者是一個map或者slice變量,而你更新了集合中的元素,或者你更新的域的接收者是指針。
package main
import"fmt"
type data struct{
num int
key *string
items map[string]bool}
func (this*data) pmethod(){this.num =7}
func (this data) vmethod(){this.num =8*this.key ="v.key"this.items["vmethod"]=true}
func main(){
key :="key.1"
d := data{1,&key,make(map[string]bool)}
fmt.Printf("num=%v key=%v items=%v\n",d.num,*d.key,d.items)//prints num=1 key=key.1 items=map[]
d.pmethod()
fmt.Printf("num=%v key=%v items=%v\n",d.num,*d.key,d.items)//prints num=7 key=key.1 items=map[]
d.vmethod()
fmt.Printf("num=%v key=%v items=%v\n",d.num,*d.key,d.items)//prints num=7 key=v.key items=map[vmethod:true]}
原文地址: levy.at/blog/11