Golang作為一個略古怪而新的語言,有自己一套特色和哲學(xué)。從其他語言轉(zhuǎn)來的開發(fā)者在剛接觸到的時候往往大吃苦頭,我也不例外。這篇文章很細致地介紹了Golang的一些常見坑點,讀完全篇中槍好多次。故將其轉(zhuǎn)載。由于文章很長,分為上下兩部分,第一部分記錄初級篇,第二部分記錄進階和高級篇:此為第二部分
目錄
初級篇
開大括號不能放在單獨的一行
未使用的變量
未使用的Imports
簡式的變量聲明僅可以在函數(shù)內(nèi)部使用
使用簡式聲明重復(fù)聲明變量
偶然的變量隱藏Accidental Variable Shadowing
不使用顯式類型,無法使用“nil”來初始化變量
使用“nil” Slices and Maps
Map的容量
字符串不會為“nil”
Array函數(shù)的參數(shù)
在Slice和Array使用“range”語句時的出現(xiàn)的不希望得到的值
Slices和Arrays是一維的
訪問不存在的Map Keys
Strings無法修改
String和Byte Slice之間的轉(zhuǎn)換
String和索引操作
字符串不總是UTF8文本
字符串的長度
在多行的Slice、Array和Map語句中遺漏逗號
log.Fatal和log.Panic不僅僅是Log
內(nèi)建的數(shù)據(jù)結(jié)構(gòu)操作不是同步的
String在“range”語句中的迭代值
對Map使用“for range”語句迭代
"switch"聲明中的失效行為
自增和自減
按位NOT操作
操作優(yōu)先級的差異
未導(dǎo)出的結(jié)構(gòu)體不會被編碼
有活動的Goroutines下的應(yīng)用退出
向無緩存的Channel發(fā)送消息,只要目標接收者準備好就會立即返回
向已關(guān)閉的Channel發(fā)送會引起Panic
使用"nil" Channels
傳值方法的接收者無法修改原有的值
進階篇
關(guān)閉HTTP的響應(yīng)
關(guān)閉HTTP的連接
比較Structs, Arrays, Slices, and Maps
從Panic中恢復(fù)
在Slice, Array, and Map "range"語句中更新引用元素的值
在Slice中"隱藏"數(shù)據(jù)
Slice的數(shù)據(jù)“毀壞”
"走味的"Slices
類型聲明和方法
從"for switch"和"for select"代碼塊中跳出
"for"聲明中的迭代變量和閉包
Defer函數(shù)調(diào)用參數(shù)的求值
被Defer的函數(shù)調(diào)用執(zhí)行
失敗的類型斷言
阻塞的Goroutine和資源泄露
高級篇
使用指針接收方法的值的實例
更新Map的值
"nil" Interfaces和"nil" Interfaces的值
棧和堆變量
GOMAXPROCS, 并發(fā), 和并行
讀寫操作的重排順序
優(yōu)先調(diào)度
進階篇
關(guān)閉HTTP的響應(yīng)
- level: intermediate
當你使用標準http庫發(fā)起請求時,你得到一個http的響應(yīng)變量。如果你不讀取響應(yīng)主體,你依舊需要關(guān)閉它。注意對于空的響應(yīng)你也一定要這么做。對于新的Go開發(fā)者而言,這個很容易就會忘掉。
一些新的Go開發(fā)者確實嘗試關(guān)閉響應(yīng)主體,但他們在錯誤的地方做。
package main
import("fmt""net/http""io/ioutil")
func main(){
resp, err := http.Get("https://api.ipify.org?format=json")
defer resp.Body.Close()//not okif err !=nil{
fmt.Println(err)return}
body, err := ioutil.ReadAll(resp.Body)if err !=nil{
fmt.Println(err)return}
fmt.Println(string(body))}
這段代碼對于成功的請求沒問題,但如果http的請求失敗, resp變量可能會是 nil,這將導(dǎo)致一個runtime panic。
最常見的關(guān)閉響應(yīng)主體的方法是在http響應(yīng)的錯誤檢查后調(diào)用 defer。
package main
import("fmt""net/http""io/ioutil")
func main(){
resp, err := http.Get("https://api.ipify.org?format=json")if err !=nil{
fmt.Println(err)return}
defer resp.Body.Close()//ok, most of the time :-)
body, err := ioutil.ReadAll(resp.Body)if err !=nil{
fmt.Println(err)return}
fmt.Println(string(body))}
大多數(shù)情況下,當你的http響應(yīng)失敗時, resp變量將為 nil,而 err變量將是 non-nil。然而,當你得到一個重定向的錯誤時,兩個變量都將是 non-nil。這意味著你最后依然會內(nèi)存泄露。
通過在http響應(yīng)錯誤處理中添加一個關(guān)閉 non-nil響應(yīng)主體的的調(diào)用來修復(fù)這個問題。另一個方法是使用一個 defer調(diào)用來關(guān)閉所有失敗和成功的請求的響應(yīng)主體。
package main
import("fmt""net/http""io/ioutil")
func main(){
resp, err := http.Get("https://api.ipify.org?format=json")if resp !=nil{
defer resp.Body.Close()}if err !=nil{
fmt.Println(err)return}
body, err := ioutil.ReadAll(resp.Body)if err !=nil{
fmt.Println(err)return}
fmt.Println(string(body))}
resp.Body.Close()的原始實現(xiàn)也會讀取并丟棄剩余的響應(yīng)主體數(shù)據(jù)。這確保了http的鏈接在keepalive http連接行為開啟的情況下,可以被另一個請求復(fù)用。最新的http客戶端的行為是不同的?,F(xiàn)在讀取并丟棄剩余的響應(yīng)數(shù)據(jù)是你的職責(zé)。如果你不這么做,http的連接可能會關(guān)閉,而無法被重用。這個小技巧應(yīng)該會寫在Go 1.5的文檔中。
如果http連接的重用對你的應(yīng)用很重要,你可能需要在響應(yīng)處理邏輯的后面添加像下面的代碼:
_, err = io.Copy(ioutil.Discard, resp.Body)
如果你不立即讀取整個響應(yīng)將是必要的,這可能在你處理json API響應(yīng)時會發(fā)生:
json.NewDecoder(resp.Body).Decode(&data)
關(guān)閉HTTP的連接
- level: intermediate
一些HTTP服務(wù)器保持會保持一段時間的網(wǎng)絡(luò)連接(根據(jù)HTTP 1.1的說明和服務(wù)器端的“keep-alive”配置)。默認情況下,標準http庫只在目標HTTP服務(wù)器要求關(guān)閉時才會關(guān)閉網(wǎng)絡(luò)連接。這意味著你的應(yīng)用在某些條件下消耗完sockets/file的描述符。
你可以通過設(shè)置請求變量中的 Close域的值為 true,來讓http庫在請求完成時關(guān)閉連接。
另一個選項是添加一個 Connection的請求頭,并設(shè)置為 close。目標HTTP服務(wù)器應(yīng)該也會響應(yīng)一個 Connection: close的頭。當http庫看到這個響應(yīng)頭時,它也將會關(guān)閉連接。
package main
import("fmt""net/http""io/ioutil")
func main(){
req, err := http.NewRequest("GET","http://golang.org",nil)if err !=nil{
fmt.Println(err)return}
req.Close=true//or do this://req.Header.Add("Connection", "close")
resp, err := http.DefaultClient.Do(req)if resp !=nil{
defer resp.Body.Close()}if err !=nil{
fmt.Println(err)return}
body, err := ioutil.ReadAll(resp.Body)if err !=nil{
fmt.Println(err)return}
fmt.Println(len(string(body)))}
你也可以取消http的全局連接復(fù)用。你將需要為此創(chuàng)建一個自定義的http傳輸配置。
package main
import("fmt""net/http""io/ioutil")
func main(){
tr :=&http.Transport{DisableKeepAlives:true}
client :=&http.Client{Transport: tr}
resp, err := client.Get("http://golang.org")if resp !=nil{
defer resp.Body.Close()}if err !=nil{
fmt.Println(err)return}
fmt.Println(resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)if err !=nil{
fmt.Println(err)return}
fmt.Println(len(string(body)))}
如果你向同一個HTTP服務(wù)器發(fā)送大量的請求,那么把保持網(wǎng)絡(luò)連接的打開是沒問題的。然而,如果你的應(yīng)用在短時間內(nèi)向大量不同的HTTP服務(wù)器發(fā)送一兩個請求,那么在引用收到響應(yīng)后立刻關(guān)閉網(wǎng)絡(luò)連接是一個好主意。增加打開文件的限制數(shù)可能也是個好主意。當然,正確的選擇源自于應(yīng)用。
比較Structs, Arrays, Slices, and Maps
- level: intermediate
如果結(jié)構(gòu)體中的各個元素都可以用你可以使用等號來比較的話,那就可以使用相號, ==,來比較結(jié)構(gòu)體變量。
package main
import"fmt"
type data struct{
num int
fp float32
complex complex64
str stringchar rune
yes bool
events <-chan string
handler interface{}ref*byte
raw [10]byte}
func main(){
v1 := data{}
v2 := data{}
fmt.Println("v1 == v2:",v1 == v2)//prints: v1 == v2: true}
如果結(jié)構(gòu)體中的元素?zé)o法比較,那使用等號將導(dǎo)致編譯錯誤。注意數(shù)組僅在它們的數(shù)據(jù)元素可比較的情況下才可以比較。
package main
import"fmt"
type data struct{
num int//ok
checks [10]func()bool//not comparable
doit func()bool//not comparable
m map[string]string//not comparable
bytes []byte//not comparable}
func main(){
v1 := data{}
v2 := data{}
fmt.Println("v1 == v2:",v1 == v2)}
Go確實提供了一些助手函數(shù),用于比較那些無法使用等號比較的變量。
最常用的方法是使用 reflect包中的 DeepEqual()函數(shù)。
package main
import("fmt""reflect")
type data struct{
num int//ok
checks [10]func()bool//not comparable
doit func()bool//not comparable
m map[string]string//not comparable
bytes []byte//not comparable}
func main(){
v1 := data{}
v2 := data{}
fmt.Println("v1 == v2:",reflect.DeepEqual(v1,v2))//prints: v1 == v2: true
m1 := map[string]string{"one":"a","two":"b"}
m2 := map[string]string{"two":"b","one":"a"}
fmt.Println("m1 == m2:",reflect.DeepEqual(m1, m2))//prints: m1 == m2: true
s1 :=[]int{1,2,3}
s2 :=[]int{1,2,3}
fmt.Println("s1 == s2:",reflect.DeepEqual(s1, s2))//prints: s1 == s2: true}
除了很慢(這個可能會也可能不會影響你的應(yīng)用), DeepEqual()也有其他自身的技巧。
package main
import("fmt""reflect")
func main(){var b1 []byte=nil
b2 :=[]byte{}
fmt.Println("b1 == b2:",reflect.DeepEqual(b1, b2))//prints: b1 == b2: false}
DeepEqual()不會認為空的slice與“nil”的slice相等。這個行為與你使用 bytes.Equal()函數(shù)的行為不同。 bytes.Equal()認為“nil”和空的slice是相等的。
package main
import("fmt""bytes")
func main(){var b1 []byte=nil
b2 :=[]byte{}
fmt.Println("b1 == b2:",bytes.Equal(b1, b2))//prints: b1 == b2: true}
DeepEqual()在比較slice時并不總是完美的。
package main
import("fmt""reflect""encoding/json")
func main(){var str string="one"varininterface{}="one"
fmt.Println("str == in:",str ==in,reflect.DeepEqual(str,in))//prints: str == in: true true
v1 :=[]string{"one","two"}
v2 :=[]interface{}{"one","two"}
fmt.Println("v1 == v2:",reflect.DeepEqual(v1, v2))//prints: v1 == v2: false (not ok)
data := map[string]interface{}{"code":200,"value":[]string{"one","two"},}
encoded, _ := json.Marshal(data)var decoded map[string]interface{}
json.Unmarshal(encoded,&decoded)
fmt.Println("data == decoded:",reflect.DeepEqual(data, decoded))//prints: data == decoded: false (not ok)}
如果你的byte slice(或者字符串)中包含文字數(shù)據(jù),而當你要不區(qū)分大小寫形式的值時(在使用 ==, bytes.Equal(),或者 bytes.Compare()),你可能會嘗試使用“bytes”和“string”包中的 ToUpper()或者 ToLower()函數(shù)。對于英語文本,這么做是沒問題的,但對于許多其他的語言來說就不行了。這時應(yīng)該使用 strings.EqualFold()和 bytes.EqualFold()。
如果你的byte slice中包含需要驗證用戶數(shù)據(jù)的隱私信息(比如,加密哈希、tokens等),不要使用 reflect.DeepEqual()、 bytes.Equal(),或者 bytes.Compare(),因為這些函數(shù)將會讓你的應(yīng)用易于被定時攻擊。為了避免泄露時間信息,使用 'crypto/subtle'包中的函數(shù)(即, subtle.ConstantTimeCompare())。
從Panic中恢復(fù)
- level: intermediate
recover()函數(shù)可以用于獲取/攔截panic。僅當在一個defer函數(shù)中被完成時,調(diào)用 recover()將會完成這個小技巧。
Incorrect:
ackage main
import"fmt"
func main(){
recover()//doesn't do anything
panic("not good")
recover()//won't be executed :)
fmt.Println("ok")}
Works:
package main
import"fmt"
func main(){
defer func(){
fmt.Println("recovered:",recover())}()
panic("not good")}
recover()的調(diào)用僅當它在defer函數(shù)中被直接調(diào)用時才有效。
Fails:
package main
import"fmt"
func doRecover(){
fmt.Println("recovered =>",recover())//prints: recovered => <nil>}
func main(){
defer func(){
doRecover()//panic is not recovered}()
panic("not good")}
在Slice, Array, and Map "range"語句中更新引用元素的值
- level: intermediate
在“range”語句中生成的數(shù)據(jù)的值是真實集合元素的拷貝。它們不是原有元素的引用。這意味著更新這些值將不會修改原來的數(shù)據(jù)。同時也意味著使用這些值的地址將不會得到原有數(shù)據(jù)的指針。
package main
import"fmt"
func main(){
data :=[]int{1,2,3}for _,v := range data {
v *=10//original item is not changed}
fmt.Println("data:",data)//prints data: [1 2 3]}
如果你需要更新原有集合中的數(shù)據(jù),使用索引操作符來獲得數(shù)據(jù)。
package main
import"fmt"
func main(){
data :=[]int{1,2,3}for i,_ := range data {
data[i]*=10}
fmt.Println("data:",data)//prints data: [10 20 30]}
如果你的集合保存的是指針,那規(guī)則會稍有不同。如果要更新原有記錄指向的數(shù)據(jù),你依然需要使用索引操作,但你可以使用 for range語句中的第二個值來更新存儲在目標位置的數(shù)據(jù)。
package main
import"fmt"
func main(){
data :=[]*struct{num int}{{1},{2},{3}}for _,v := range data {
v.num *=10}
fmt.Println(data[0],data[1],data[2])//prints &{10} &{20} &{30}}
在Slice中"隱藏"數(shù)據(jù)
- level: intermediate
當你重新劃分一個slice時,新的slice將引用原有slice的數(shù)組。如果你忘了這個行為的話,在你的應(yīng)用分配大量臨時的slice用于創(chuàng)建新的slice來引用原有數(shù)據(jù)的一小部分時,會導(dǎo)致難以預(yù)期的內(nèi)存使用。
package main
import"fmt"
func get()[]byte{
raw := make([]byte,10000)
fmt.Println(len(raw),cap(raw),&raw[0])//prints: 10000 10000 <byte_addr_x>return raw[:3]}
func main(){
data :=get()
fmt.Println(len(data),cap(data),&data[0])//prints: 3 10000 <byte_addr_x>}
為了避免這個陷阱,你需要從臨時的slice中拷貝數(shù)據(jù)(而不是重新劃分slice)。
package main
import"fmt"
func get()[]byte{
raw := make([]byte,10000)
fmt.Println(len(raw),cap(raw),&raw[0])//prints: 10000 10000 <byte_addr_x>
res := make([]byte,3)
copy(res,raw[:3])return res
}
func main(){
data :=get()
fmt.Println(len(data),cap(data),&data[0])//prints: 3 3 <byte_addr_y>}
Slice的數(shù)據(jù)“毀壞”
- level: intermediate
比如說你需要重新一個路徑(在slice中保存)。你通過修改第一個文件夾的名字,然后把名字合并來創(chuàng)建新的路勁,來重新劃分指向各個文件夾的路徑。
package main
import("fmt""bytes")
func main(){
path :=[]byte("AAAA/BBBBBBBBB")
sepIndex := bytes.IndexByte(path,'/')
dir1 := path[:sepIndex]
dir2 := path[sepIndex+1:]
fmt.Println("dir1 =>",string(dir1))//prints: dir1 => AAAA
fmt.Println("dir2 =>",string(dir2))//prints: dir2 => BBBBBBBBB
dir1 = append(dir1,"suffix"...)
path = bytes.Join([][]byte{dir1,dir2},[]byte{'/'})
fmt.Println("dir1 =>",string(dir1))//prints: dir1 => AAAAsuffix
fmt.Println("dir2 =>",string(dir2))//prints: dir2 => uffixBBBB (not ok)
fmt.Println("new path =>",string(path))}
結(jié)果與你想的不一樣。與"AAAAsuffix/BBBBBBBBB"相反,你將會得到"AAAAsuffix/uffixBBBB"。這個情況的發(fā)生是因為兩個文件夾的slice都潛在的引用了同一個原始的路徑slice。這意味著原始路徑也被修改了。根據(jù)你的應(yīng)用,這也許會是個問題。
通過分配新的slice并拷貝需要的數(shù)據(jù),你可以修復(fù)這個問題。另一個選擇是使用完整的slice表達式。
package main
import("fmt""bytes")
func main(){
path :=[]byte("AAAA/BBBBBBBBB")
sepIndex := bytes.IndexByte(path,'/')
dir1 := path[:sepIndex:sepIndex]//full slice expression
dir2 := path[sepIndex+1:]
fmt.Println("dir1 =>",string(dir1))//prints: dir1 => AAAA
fmt.Println("dir2 =>",string(dir2))//prints: dir2 => BBBBBBBBB
dir1 = append(dir1,"suffix"...)
path = bytes.Join([][]byte{dir1,dir2},[]byte{'/'})
fmt.Println("dir1 =>",string(dir1))//prints: dir1 => AAAAsuffix
fmt.Println("dir2 =>",string(dir2))//prints: dir2 => BBBBBBBBB (ok now)
fmt.Println("new path =>",string(path))}
完整的slice表達式中的額外參數(shù)可以控制新的slice的容量。現(xiàn)在在那個slice后添加元素將會觸發(fā)一個新的buffer分配,而不是覆蓋第二個slice中的數(shù)據(jù)。
"走味的"Slices
- level: intermediate
多個slice可以引用同一個數(shù)據(jù)。比如,當你從一個已有的slice創(chuàng)建一個新的slice時,這就會發(fā)生。如果你的應(yīng)用功能需要這種行為,那么你將需要關(guān)注下“走味的”slice。
在某些情況下,在一個slice中添加新的數(shù)據(jù),在原有數(shù)組無法保持更多新的數(shù)據(jù)時,將導(dǎo)致分配一個新的數(shù)組。而現(xiàn)在其他的slice還指向老的數(shù)組(和老的數(shù)據(jù))。
import"fmt"
func main(){
s1 :=[]int{1,2,3}
fmt.Println(len(s1),cap(s1),s1)//prints 3 3 [1 2 3]
s2 := s1[1:]
fmt.Println(len(s2),cap(s2),s2)//prints 2 2 [2 3]for i := range s2 { s2[i]+=20}//still referencing the same array
fmt.Println(s1)//prints [1 22 23]
fmt.Println(s2)//prints [22 23]
s2 = append(s2,4)for i := range s2 { s2[i]+=10}//s1 is now "stale"
fmt.Println(s1)//prints [1 22 23]
fmt.Println(s2)//prints [32 33 14]}
類型聲明和方法
- level: intermediate
當你通過把一個現(xiàn)有(非interface)的類型定義為一個新的類型時,新的類型不會繼承現(xiàn)有類型的方法。
Fails:
package main
import"sync"
type myMutex sync.Mutex
func main(){var mtx myMutex
mtx.Lock()//error
mtx.Unlock()//error }
Compile Errors:
/tmp/sandbox106401185/main.go:9: mtx.Lockundefined(type myMutex has no field or method Lock)/tmp/sandbox106401185/main.go:10: mtx.Unlockundefined(type myMutex has no field or method Unlock)
如果你確實需要原有類型的方法,你可以定義一個新的struct類型,用匿名方式把原有類型嵌入其中。
Works:
package main
import"sync"
type myLocker struct{
sync.Mutex}
func main(){varlock myLocker
lock.Lock()//oklock.Unlock()//ok}
interface類型的聲明也會保留它們的方法集合。
Works:package main
import"sync"
type myLocker sync.Locker
func main(){varlock myLocker =new(sync.Mutex)lock.Lock()//oklock.Unlock()//ok}
從"for switch"和"for select"代碼塊中跳出
- level: intermediate
沒有標簽的“break”聲明只能從內(nèi)部的switch/select代碼塊中跳出來。如果無法使用“return”聲明的話,那就為外部循環(huán)定義一個標簽是另一個好的選擇。
package main
import"fmt"
func main(){
loop:for{switch{casetrue:
fmt.Println("breaking out...")break loop
}}
fmt.Println("out!")}
"goto"聲明也可以完成這個功能。。。
"for"聲明中的迭代變量和閉包
- level: intermediate
這在Go中是個很常見的技巧。 for語句中的迭代變量在每次迭代時被重新使用。這就意味著你在 for循環(huán)中創(chuàng)建的閉包(即函數(shù)字面量)將會引用同一個變量(而在那些goroutine開始執(zhí)行時就會得到那個變量的值)。
Incorrect:
package main
import("fmt""time")
func main(){
data :=[]string{"one","two","three"}for _,v := range data {
go func(){
fmt.Println(v)}()}
time.Sleep(3* time.Second)//goroutines print: three, three, three}
最簡單的解決方法(不需要修改goroutine)是,在 for循環(huán)代碼塊內(nèi)把當前迭代的變量值保存到一個局部變量中。
Works:
package main
import("fmt""time")
func main(){
data :=[]string{"one","two","three"}for _,v := range data {
vcopy := v //
go func(){
fmt.Println(vcopy)}()}
time.Sleep(3* time.Second)//goroutines print: one, two, three}
另一個解決方法是把當前的迭代變量作為匿名goroutine的參數(shù)。
Works:
package main
import("fmt""time")
func main(){
data :=[]string{"one","two","three"}for _,v := range data {
go func(instring){
fmt.Println(in)}(v)}
time.Sleep(3* time.Second)//goroutines print: one, two, three}
下面這個陷阱稍微復(fù)雜一些的版本。
Incorrect:
package main
import("fmt""time")
type field struct{
name string}
func (p *field)print(){
fmt.Println(p.name)}
func main(){
data :=[]field{{"one"},{"two"},{"three"}}for _,v := range data {
go v.print()}
time.Sleep(3* time.Second)//goroutines print: three, three, three}
Works:
package main
import("fmt""time")
type field struct{
name string}
func (p *field)print(){
fmt.Println(p.name)}
func main(){
data :=[]field{{"one"},{"two"},{"three"}}for _,v := range data {
v := v
go v.print()}
time.Sleep(3* time.Second)//goroutines print: one, two, three}
在運行這段代碼時你認為會看到什么結(jié)果?(原因是什么?)
package main
import("fmt""time")
type field struct{
name string}
func (p *field)print(){
fmt.Println(p.name)}
func main(){
data :=[]*field{{"one"},{"two"},{"three"}}for _,v := range data {
go v.print()}
time.Sleep(3* time.Second)}
Defer函數(shù)調(diào)用參數(shù)的求值
- level: intermediate
被defer的函數(shù)的參數(shù)會在defer聲明時求值(而不是在函數(shù)實際執(zhí)行時)。
Arguments for a deferred function call are evaluated when the defer statement is evaluated (not when the function is actually executing).
package main
import"fmt"
func main(){var i int=1
defer fmt.Println("result =>",func()int{return i *2}())
i++//prints: result => 2 (not ok if you expected 4)}
被Defer的函數(shù)調(diào)用執(zhí)行
- level: intermediate
被defer的調(diào)用會在包含的函數(shù)的末尾執(zhí)行,而不是包含代碼塊的末尾。對于Go新手而言,一個很常犯的錯誤就是無法區(qū)分被defer的代碼執(zhí)行規(guī)則和變量作用規(guī)則。如果你有一個長時運行的函數(shù),而函數(shù)內(nèi)有一個 for循環(huán)試圖在每次迭代時都 defer資源清理調(diào)用,那就會出現(xiàn)問題。
package main
import("fmt""os""path/filepath")
func main(){if len(os.Args)!=2{
os.Exit(-1)}
start, err := os.Stat(os.Args[1])if err !=nil||!start.IsDir(){
os.Exit(-1)}var targets []string
filepath.Walk(os.Args[1], func(fpath string,fi os.FileInfo, err error) error {if err !=nil{return err
}if!fi.Mode().IsRegular(){returnnil}
targets = append(targets,fpath)returnnil})for _,target := range targets {
f, err := os.Open(target)if err !=nil{
fmt.Println("bad target:",target,"error:",err)//prints error: too many open filesbreak}
defer f.Close()//will not be closed at the end of this code block//do something with the file...}}
解決這個問題的一個方法是把代碼塊寫成一個函數(shù)。
package main
import("fmt""os""path/filepath")
func main(){if len(os.Args)!=2{
os.Exit(-1)}
start, err := os.Stat(os.Args[1])if err !=nil||!start.IsDir(){
os.Exit(-1)}var targets []string
filepath.Walk(os.Args[1], func(fpath string,fi os.FileInfo, err error) error {if err !=nil{return err
}if!fi.Mode().IsRegular(){returnnil}
targets = append(targets,fpath)returnnil})for _,target := range targets {
func(){
f, err := os.Open(target)if err !=nil{
fmt.Println("bad target:",target,"error:",err)return}
defer f.Close()//ok//do something with the file...}()}}
另一個方法是去掉 defer語句 :-)
失敗的類型斷言
- level: intermediate
失敗的類型斷言返回斷言聲明中使用的目標類型的“零值”。這在與隱藏變量混合時,會發(fā)生未知情況。
Incorrect:
package main
import"fmt"
func main(){var data interface{}="great"if data, ok := data.(int); ok {
fmt.Println("[is an int] value =>",data)}else{
fmt.Println("[not an int] value =>",data)//prints: [not an int] value => 0 (not "great")}}
Works:
package main
import"fmt"
func main(){var data interface{}="great"if res, ok := data.(int); ok {
fmt.Println("[is an int] value =>",res)}else{
fmt.Println("[not an int] value =>",data)//prints: [not an int] value => great (as expected)}}
阻塞的Goroutine和資源泄露
- level: intermediate
Rob Pike在2012年的Google I/O大會上所做的“Go Concurrency Patterns”的演講上,說道過幾種基礎(chǔ)的并發(fā)模式。從一組目標中獲取第一個結(jié)果就是其中之一。
func First(query string, replicas ...Search)Result{
c := make(chan Result)
searchReplica := func(i int){ c <- replicas[i](query)}for i := range replicas {
go searchReplica(i)}return<-c
}
這個函數(shù)在每次搜索重復(fù)時都會起一個goroutine。每個goroutine把它的搜索結(jié)果發(fā)送到結(jié)果的channel中。結(jié)果channel的第一個值被返回。
那其他goroutine的結(jié)果會怎樣呢?還有那些goroutine自身呢?
在 First()函數(shù)中的結(jié)果channel是沒緩存的。這意味著只有第一個goroutine返回。其他的goroutine會困在嘗試發(fā)送結(jié)果的過程中。這意味著,如果你有不止一個的重復(fù)時,每個調(diào)用將會泄露資源。
為了避免泄露,你需要確保所有的goroutine退出。一個不錯的方法是使用一個有足夠保存所有緩存結(jié)果的channel。
func First(query string, replicas ...Search)Result{
c := make(chan Result,len(replicas))
searchReplica := func(i int){ c <- replicas[i](query)}for i := range replicas {
go searchReplica(i)}return<-c
}
另一個不錯的解決方法是使用一個有 default情況的 select語句和一個保存一個緩存結(jié)果的channel。 default情況保證了即使當結(jié)果channel無法收到消息的情況下,goroutine也不會堵塞。
func First(query string, replicas ...Search)Result{
c := make(chan Result,1)
searchReplica := func(i int){select{case c <- replicas[i](query):default:}}for i := range replicas {
go searchReplica(i)}return<-c
}
你也可以使用特殊的取消channel來終止workers。
func First(query string, replicas ...Search)Result{
c := make(chan Result)done:= make(chan struct{})
defer close(done)
searchReplica := func(i int){select{case c <- replicas[i](query):case<-done:}}for i := range replicas {
go searchReplica(i)}return<-c
}
為何在演講中會包含這些bug?Rob Pike僅僅是不想把演示復(fù)雜化。這么作是合理的,但對于Go新手而言,可能會直接使用代碼,而不去思考它可能有問題。
高級篇
使用指針接收方法的值的實例
- level: advanced
只要值是可取址的,那在這個值上調(diào)用指針接收方法是沒問題的。換句話說,在某些情況下,你不需要在有一個接收值的方法版本。
然而并不是所有的變量是可取址的。Map的元素就不是。通過interface引用的變量也不是。
package main
import"fmt"
type data struct{
name string}
func (p *data)print(){
fmt.Println("name:",p.name)}
type printer interface{print()}
func main(){
d1 := data{"one"}
d1.print()//okvarin printer = data{"two"}//errorin.print()
m := map[string]data {"x":data{"three"}}
m["x"].print()//error}
Compile Errors:
/tmp/sandbox017696142/main.go:21: cannot use data literal (type data)as type printer in assignment: data does not implement printer (print method has pointer receiver)/tmp/sandbox017696142/main.go:25: cannot call pointer method on m["x"]/tmp/sandbox017696142/main.go:25: cannot take the address of m["x"]
更新Map的值
- level: advanced
如果你有一個struct值的map,你無法更新單個的struct值。
Fails:
package main
type data struct{
name string}
func main(){
m := map[string]data {"x":{"one"}}
m["x"].name ="two"http://error}
Compile Error:
/tmp/sandbox380452744/main.go:9: cannot assign to m["x"].name
這個操作無效是因為map元素是無法取址的。
而讓Go新手更加困惑的是slice元素是可以取址的。
package main
import"fmt"
type data struct{
name string}
func main(){
s :=[]data {{"one"}}
s[0].name ="two"http://ok
fmt.Println(s)//prints: [{two}]}
注意在不久之前,使用編譯器之一(gccgo)是可以更新map的元素值的,但這一行為很快就被修復(fù)了 :-)它也被認為是Go 1.3的潛在特性。在那時還不是要急需支持的,但依舊在todo list中。
第一個有效的方法是使用一個臨時變量。
package main
import"fmt"
type data struct{
name string}
func main(){
m := map[string]data {"x":{"one"}}
r := m["x"]
r.name ="two"
m["x"]= r
fmt.Printf("%v",m)//prints: map[x:{two}]}
另一個有效的方法是使用指針的map。
package main
import"fmt"
type data struct{
name string}
func main(){
m := map[string]*data {"x":{"one"}}
m["x"].name ="two"http://ok
fmt.Println(m["x"])//prints: &{two}}
順便說下,當你運行下面的代碼時會發(fā)生什么?
package main
type data struct{
name string}
func main(){
m := map[string]*data {"x":{"one"}}
m["z"].name ="what?"http://???}
"nil" Interfaces和"nil" Interfaces的值
- level: advanced
這在Go中是第二最常見的技巧,因為interface雖然看起來像指針,但并不是指針。interface變量僅在類型和值為“nil”時才為“nil”。
interface的類型和值會根據(jù)用于創(chuàng)建對應(yīng)interface變量的類型和值的變化而變化。當你檢查一個interface變量是否等于“nil”時,這就會導(dǎo)致未預(yù)期的行為。
package main
import"fmt"
func main(){var data *bytevarininterface{}
fmt.Println(data,data ==nil)//prints: <nil> true
fmt.Println(in,in==nil)//prints: <nil> truein= data
fmt.Println(in,in==nil)//prints: <nil> false//'data' is 'nil', but 'in' is not 'nil'}
當你的函數(shù)返回interface時,小心這個陷阱。
Incorrect:
package main
import"fmt"
func main(){
doit := func(arg int)interface{}{var result *struct{}=nilif(arg >0){
result =&struct{}{}}return result
}if res := doit(-1); res !=nil{
fmt.Println("good result:",res)//prints: good result: <nil>//'res' is not 'nil', but its value is 'nil'}}
Works:
package main
import"fmt"
func main(){
doit := func(arg int)interface{}{var result *struct{}=nilif(arg >0){
result =&struct{}{}}else{returnnil//return an explicit 'nil'}return result
}if res := doit(-1); res !=nil{
fmt.Println("good result:",res)}else{
fmt.Println("bad result (res is nil)")//here as expected}}
棧和堆變量
- level: advanced
你并不總是知道變量是分配到棧還是堆上。在C++中,使用 new創(chuàng)建的變量總是在堆上。在Go中,即使是使用 new()或者 make()函數(shù)來分配,變量的位置還是由編譯器決定。編譯器根據(jù)變量的大小和“泄露分析”的結(jié)果來決定其位置。這也意味著在局部變量上返回引用是沒問題的,而這在C或者C++這樣的語言中是不行的。
如果你想知道變量分配的位置,在“go build”或“go run”上傳入“-m“ gc標志(即, go run -gcflags -m app.go)。
GOMAXPROCS, 并發(fā), 和并行
- level: advanced
默認情況下,Go僅使用一個執(zhí)行上下文/OS線程(在當前的版本)。這個數(shù)量可以通過設(shè)置 GOMAXPROCS來提高。
一個常見的誤解是, GOMAXPROCS表示了CPU的數(shù)量,Go將使用這個數(shù)量來運行g(shù)oroutine。而 runtime.GOMAXPROCS()函數(shù)的文檔讓人更加的迷茫。 GOMAXPROCS變量描述(https://golang.org/pkg/runtime/)所討論OS線程的內(nèi)容比較好。
你可以設(shè)置 GOMAXPROCS的數(shù)量大于CPU的數(shù)量。 GOMAXPROCS的最大值是256。
package main
import("fmt""runtime")
func main(){
fmt.Println(runtime.GOMAXPROCS(-1))//prints: 1
fmt.Println(runtime.NumCPU())//prints: 1 (on play.golang.org)
runtime.GOMAXPROCS(20)
fmt.Println(runtime.GOMAXPROCS(-1))//prints: 20
runtime.GOMAXPROCS(300)
fmt.Println(runtime.GOMAXPROCS(-1))//prints: 256}
讀寫操作的重排順序
- level: advanced
Go可能會對某些操作進行重新排序,但它能保證在一個goroutine內(nèi)的所有行為順序是不變的。然而,它并不保證多goroutine的執(zhí)行順序。
package main
import("runtime""time")var _ = runtime.GOMAXPROCS(3)var a, b int
func u1(){
a =1
b =2}
func u2(){
a =3
b =4}
func p(){
println(a)
println(b)}
func main(){
go u1()
go u2()
go p()
time.Sleep(1* time.Second)}
如果你多運行幾次上面的代碼,你可能會發(fā)現(xiàn) a和 b變量有多個不同的組合:
1234020014
a和 b最有趣的組合式是 "02"。這表明 b在 a之前更新了。
如果你需要在多goroutine內(nèi)放置讀寫順序的變化,你將需要使用channel,或者使用"sync"包構(gòu)建合適的結(jié)構(gòu)體。
優(yōu)先調(diào)度
- level: advanced
有可能會出現(xiàn)這種情況,一個無恥的goroutine阻止其他goroutine運行。當你有一個不讓調(diào)度器運行的 for循環(huán)時,這就會發(fā)生。
package main
import"fmt"
func main(){done:=false
go func(){done=true}()for!done{}
fmt.Println("done!")}
for循環(huán)并不需要是空的。只要它包含了不會觸發(fā)調(diào)度執(zhí)行的代碼,就會發(fā)生這種問題。
調(diào)度器會在GC、“go”聲明、阻塞channel操作、阻塞系統(tǒng)調(diào)用和lock操作后運行。它也會在非內(nèi)聯(lián)函數(shù)調(diào)用后執(zhí)行。
package main
import"fmt"
func main(){done:=false
go func(){done=true}()for!done{
fmt.Println("not done!")//not inlined}
fmt.Println("done!")}
要想知道你在 for循環(huán)中調(diào)用的函數(shù)是否是內(nèi)聯(lián)的,你可以在“go build”或“go run”時傳入“-m” gc標志(如, go build -gcflags -m)。
另一個選擇是顯式的喚起調(diào)度器。你可以使用“runtime”包中的 Goshed()函數(shù)。
package main
import("fmt""runtime")
func main(){done:=false
go func(){done=true}()for!done{
runtime.Gosched()}
fmt.Println("done!")}
Tags: golang, 翻譯, 常見錯誤
/* * * CONFIGURATION VARIABLES * * */
var disqus_shortname = 'levysink';
var disqus_config = function () {
this.page.url = [location.protocol, '//', location.host, location.pathname].join('');
this.page.identifier = [location.protocol, '//', location.host, location.pathname].join('');
};
/* * * DON'T EDIT BELOW THIS LINE * * */
(function() {
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
})();
Please enable JavaScript to view the [comments powered by Disqus.](https://disqus.com/?ref_noscript)
原文地址: levy.at/blog/11