Go避坑指南:這些錯誤你犯過嗎?
雖然 Go 容易學(xué)習(xí),但新手還是比較容易犯一些錯誤的。本文總結(jié)了 5 個常見的錯誤,你檢驗下自己犯過沒有?!
1、循環(huán)內(nèi)部
有幾種方法可以弄清楚一個循環(huán)內(nèi)的混亂情況。
1.1、使用引用來循環(huán)迭代器變量
出于效率考慮,經(jīng)常使用單個變量來循環(huán)迭代器。由于在每次循環(huán)迭代中會有不同的值,有些時候這會導(dǎo)致未知的行為。例如:
in := []int{1, 2, 3}
var out []*int
for _, v := range in {
out = append(out, &v)
}
fmt.Println("Values:", *out[0], *out[1], *out[2])
fmt.Println("Addresses:", out[0], out[1], out[2])
輸出結(jié)果:
Values: 3 3 3
Addresses: 0xc000014188 0xc000014188 0xc000014188
是不是很驚訝?在 out 這個 slice 中的元素都是 3。實際上很容易解釋為什么會這樣:在每次迭代中,我們都將 v append 到 out 切片中。因為 v 是單個變量(內(nèi)存地址不變),每次迭代都采用新值。在輸出的第二行證明了地址是相同的,并且它們都指向相同的值。
簡單的解決方法是將循環(huán)迭代器變量復(fù)制到新變量中:
in := []int{1, 2, 3}
var out []*int
for _, v := range in {
v := v
out = append(out, &v)
}
fmt.Println("Values:", *out[0], *out[1], *out[2])
fmt.Println("Addresses:", out[0], out[1], out[2])
新的輸出:
Values: 1 2 3
Addresses: 0xc0000b6010 0xc0000b6018 0xc0000b6020
在 goroutine 中使用循環(huán)迭代變量會有相同的問題。
list := []int{1, 2, 3}
for _, v := range list {
go func() {
fmt.Printf("%d ", v)
}()
}
輸出將是:
3 3 3
可以使用上述完全相同的解決方案進行修復(fù)。請注意,如果不使用 goroutine 運行該函數(shù),則代碼將按預(yù)期運行。
這個錯誤犯錯率是很高的,要特別注意??!
1.2、在循環(huán)中調(diào)用 WaitGroup.Wait
看一段代碼:
var wg sync.WaitGroup
wg.Add(len(tasks))
for _, t := range tasks {
go func(t *task) {
defer group.Done()
}(t)
// group.Wait()
}
group.Wait()
WaitGroup 常用來等待多個 goroutine 運行完成。但如果 Wait 在循環(huán)內(nèi)部調(diào)用,即代碼中第 7 行的位置,得到的結(jié)果就不是預(yù)期的了。這個錯誤犯錯率應(yīng)該比較低。
1.3、循環(huán)內(nèi)使用 defer
因為 defer 的執(zhí)行時機是函數(shù)返回前。所以,一般不應(yīng)該在循環(huán)內(nèi)部使用 defer,除非你很清楚自己在干什么。
看一段代碼:
var mutex sync.Mutex
type Person struct {
Age int
}
persons := make([]Person, 10)
for _, p := range persons {
mutex.Lock()
// defer mutex.Unlock()
p.Age = 13
mutex.Unlock()
}
在上面的示例中,如果使用第 8 行而不是第 10 行,則下一次迭代將無法獲得互斥鎖,因為該鎖并沒有釋放,所以循環(huán)會永遠阻塞。
如果你確實需要使用在循環(huán)內(nèi)部使用 defer,則通過委托給另外一個函數(shù)的方式進行:
var mutex sync.Mutex
type Person struct {
Age int
}
persons := make([]Person, 10)
for _, p := range persons {
func() {
mutex.Lock()
defer mutex.Unlock()
p.Age = 13
}()
}
2、channel 堵塞
一般認為 goroutine + channel 是 Go 的利器。Go 強調(diào)不要通過共享內(nèi)存來通訊,而是通過通訊來共享內(nèi)存。
但在使用 channel 的過程中,需要注意堵塞問題,避免導(dǎo)致 goroutine 泄露。比如下面的代碼:
func doReq(timeout time.Duration) obj {
// ch :=make(chan obj)
ch := make(chan obj, 1)
go func() {
obj := do()
ch <- result
} ()
select {
case result = <- ch:
return result
case <- time.After(timeout):
return nil
}
}
檢查一下上面的代碼的 doReq 函數(shù),在第 4 行創(chuàng)建一個子 goroutine 來處理請求,這是 Go 服務(wù)器程序中的常見做法。
子 goroutine 執(zhí)行 do 函數(shù)并通過第 6 行的通道 ch 將結(jié)果發(fā)送回父 goroutine。子 goroutine 將在第 6 行阻塞,直到父 goroutine 在第 9 行從 ch 接收到結(jié)果為止。同時,父 goroutine 將在 select 阻塞,直到子 goroutine 將結(jié)果發(fā)送給 ch(第 9 行)或超時(第 11 行)。如果超時先發(fā)生,則父 goroutine 將從 doReq 第 12 行返回,這會導(dǎo)致沒有 goroutine 從 ch 讀取數(shù)據(jù),子 goroutine 就會一直堵塞在第 6 行。解決辦法是將 ch 從無緩沖的通道改為有緩沖的通道,因此子goroutine 即使在父 goroutine 退出后也始終可以發(fā)送結(jié)果。
這個錯誤出現(xiàn)概率不會低。還有特別要注意的一點,就是 time.After 導(dǎo)致的內(nèi)存泄露問題,只要注意程序不是頻繁執(zhí)行上面的 select 即可(畢竟 time.After 到時間了還是會回收資源的)。
3、不使用接口
接口可以使代碼更靈活。這是在代碼中引入多態(tài)的一種方法。接口允許你定義一組行為而不是特定類型。不使用接口可能不會導(dǎo)致任何錯誤,但是會導(dǎo)致代碼簡單性,靈活性和擴展性降低。
在 Go 接口中, io.Reader 和 io.Writer 可能是使用最多的。
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
這些接口非常強大,假設(shè)你要將對象寫入文件,你可以定義了一個 Save 方法:
func (o *obj) Save(file os.File) error
如果第二天,你想寫入 http.ResponseWriter,顯然不太適合再創(chuàng)建另外一個 Save 方法,這時應(yīng)該用 io.Writer:
func (o *obj) Save(w io.Writer) error
另外,你應(yīng)該知道的重要注意事項是,始終關(guān)注行為。在上面的示例中,雖然 io.ReadWriteCloser 也可以使用,但你只需要 Write 方法。接口越大,抽象性越弱。在 Go 中,通常提倡小接口。
所以,我們應(yīng)該優(yōu)先考慮使用接口,而不是具體類型。
4、不注意結(jié)構(gòu)體字段順序
這個問題不會導(dǎo)致程序錯誤,但是可能會占用更多內(nèi)存。
看一個例子:
type BadOrderedPerson struct {
Veteran bool // 1 byte
Name string // 16 byte
Age int32 // 4 byte
}
type OrderedPerson struct {
Name string
Age int32
Veteran bool
}
看起來這兩個類型都占用的空間都是 21字節(jié),但是結(jié)果卻不是這樣。我們使用 GOARCH=amd64 編譯代碼,發(fā)現(xiàn) BadOrderedPerson 類型占用 32 個字節(jié),而 OrderedPerson 類型只占用 24 個字節(jié)。為什么?原因是數(shù)據(jù)結(jié)構(gòu)對齊。在 64 位體系結(jié)構(gòu)中,內(nèi)存分配連續(xù)的 8 字節(jié)數(shù)據(jù)。需要添加的填充可以通過以下方式計算:
padding = (align - (offset mod align)) mod align
aligned = offset + padding
= offset + ((align - (offset mod align)) mod align)
type BadOrderedPerson struct {
Veteran bool // 1 byte
_ [7]byte // 7 byte: padding for alignment
Name string // 16 byte
Age int32 // 4 byte
_ struct{} // to prevent unkeyed literals
// zero sized values, like struct{} and [0]byte occurring at
// the end of a structure are assumed to have a size of one byte.
// so padding also will be addedd here as well.
}
type OrderedPerson struct {
Name string
Age int32
Veteran bool
_ struct{}
}
當你使用大型常用類型時,可能會導(dǎo)致性能問題。但是不用擔(dān)心,你不必手動處理所有結(jié)構(gòu)。這工具可以輕松的解決此類問題:https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/fieldalignment。
5、測試中不使用 race 探測器
數(shù)據(jù)爭用會導(dǎo)致莫名的故障,通常是在代碼已部署到線上很久之后才出現(xiàn)。因此,它們是并發(fā)系統(tǒng)中最常見且最難調(diào)試的錯誤類型。為了幫助區(qū)分此類錯誤,Go 1.1 引入了內(nèi)置的數(shù)據(jù)爭用檢測器(race detector)??梢院唵蔚靥砑?-race flag 來使用。
$ go test -race pkg # to test the package
$ go run -race pkg.go # to run the source file
$ go build -race # to build the package
$ go install -race pkg # to install the package
啟用數(shù)據(jù)爭用檢測器后,編譯器將記錄在代碼中何時以及如何訪問內(nèi)存,而 runtime 監(jiān)控對共享變量的非同步訪問。
找到數(shù)據(jù)競爭后,競爭檢測器將打印一份報告,其中包含用于沖突訪問的堆棧跟蹤。這是一個例子:
WARNING: DATA RACE
Read by goroutine 185:
net.(*pollServer).AddFD()
src/net/fd_unix.go:89 +0x398
net.(*pollServer).WaitWrite()
src/net/fd_unix.go:247 +0x45
net.(*netFD).Write()
src/net/fd_unix.go:540 +0x4d4
net.(*conn).Write()
src/net/net.go:129 +0x101
net.func·060()
src/net/timeout_test.go:603 +0xaf
Previous write by goroutine 184:
net.setWriteDeadline()
src/net/sockopt_posix.go:135 +0xdf
net.setDeadline()
src/net/sockopt_posix.go:144 +0x9c
net.(*conn).SetDeadline()
src/net/net.go:161 +0xe3
net.func·061()
src/net/timeout_test.go:616 +0x3ed
Goroutine 185 (running) created at:
net.func·061()
src/net/timeout_test.go:609 +0x288
Goroutine 184 (running) created at:
net.TestProlongTimeout()
src/net/timeout_test.go:618 +0x298
testing.tRunner()
src/testing/testing.go:301 +0xe8
總結(jié)
錯誤不可怕,但我們需要從錯誤中吸取教訓(xùn),避免再次掉入同樣的坑里。掉入一個坑, 我們應(yīng)該想辦法探究出原因,知道為什么,下次再掉坑的可能性就會小很多。
除了以上這幾點,還有哪些你常碰到的錯誤或坑呢?歡迎留言交流!
推薦閱讀
