告誡 Go 初學者,千萬別掉進這 5 個坑
初學 golang 我們經常會犯一些錯誤,雖然它們不會產生類型檢查的異常,但是它們往往潛在影響軟件的功能。
- 循環中易犯的錯誤
1.1 使用循環迭代變量的指針
先來看一段代碼
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])
結果輸出:
Values: 3 3 3
Addresses: 0xc0000a4008 0xc0000a4008 0xc0000a4008
你可能會很奇怪爲什麼會出現這種情況,結果不應該是 1 2 3 和三個不同的地址嗎?其實真實原因 for range 過程中創建了每個元素的副本,而不是直接返回每個元素的引用。v 在 for 循環引進的一個塊作用域內進行聲明,它是一個共享的可訪問的地址。在迭代過程中,返回的變量是根據切片依次賦值的到變量 v 中,故而值的地址總是相同的,導致結果不如預期。那麼該如何修改呢?
最簡單的做法是將循環迭代變量複製到新的變量中:
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])
PS: 也可以直接根據 range 返回第一個參數作爲數組索引下標 拿值
循環中 goroutine 使用循環迭代變量也會存在同樣的問題:
list := []int{1, 2, 3}
for _, v := range list {
go func() {
fmt.Printf("%d ", v)
}()
}
輸出結果:
3 3 3
1.2 循環中調用 WaitGroup.Wait
按照 WaitGroup 的正常用法,當 wg.Done() 被調用 len(tasks) 次,wg.Wait() 會被自動解除阻塞。當時下面代碼中,將 wg.wait() 放到循環中後,導致第二次循環被阻塞,解決辦法 將 wg.wait() 移除循環即可。
var wg sync.WaitGroup
wg.Add(len(tasks))
for _, t := range tasks {
go func(t *task) {
defer wg.Done()
}(t)
// wg.Wait()
}
wg.Wait()
1.3 循環中使用 defer
defer 是在函數返回的時候才執行,除非我們知道自己在做什麼否則你不應該在循環中使用 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 行代碼,則下一次循環將因爲無法獲取排他鎖永遠被阻塞。
如果你真的想在內循環中使用 defer,你很可能是想委託其他函數來完成任務。
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
}()
}
但是,有時在循環中使用 defer 確實比較方便,但是你真的應該知道你在做什麼。Go 不能容忍愚蠢的人。
- 發送到一個無保證的 channel
我們可以在一個 goroutine 中發送數據到 channels,在另一個 goroutine 中接收這些數據。默認情況下,發送和接收會阻塞直到對方 ready。這使得 goroutines 可以不用顯式使用鎖或條件變量就可以完成同步操作。
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 函數創建了一個子 Goroutine 來處理請求,這在 go 服務端程序中是常見的做法。子 Goroutine 執行 do 函數並通過 channel 發送結果給父節點。子 Goroutine 將會阻塞直到父節點從 channel 中收到數據。與此同時,父節點也會阻塞在 select 上,直到子 Goroutine 發送結果到 channel,或者超時。當超時先發生,則會執行第 12 行代碼並且子 Goroutine 將永遠阻塞。
解決方案:
-
將 ch 從無緩衝 channel 改成有緩衝 channel,這樣子 Goroutine 將永遠可以發送結果數據,即使父節點已經退出
-
select 中使用 default 語句,如果沒有 goroutine 收到 ch,則會發送默認情況。儘管這種方案不是總能生效。
- 不使用接口
接口的使用可以讓我們的代碼更加靈活,也是一種在代碼中引入多態的方法。接口允許我們請求一組行爲而不是特定類型。不使用接口不會產生任何錯誤,但是它會導致我們的代碼不簡潔、不靈活、並且不具備可拓展性。
衆多接口中,io.Reader 和 io.Writer 可能是最受歡迎的。
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
這些接口功能非常強大。假設你要向一個文件中寫入數據,你會定義一個 save 方法:
func (o *obj)Save(file os.File) error
但是第二天你又想往 http.ResponseWriter 中寫入數據,但是你不想再定義一個新的方法,怎麼辦?使用 io.Writer
func (o *obj)Save(w io.Writer) error
還有一個重點注意的事項,你應該知道總是請求你要使用的行爲。上面的例子中,請求一個 io.ReadWriteCloser 也可以正常工作,但它不是一個最佳實踐,因爲我們只是想使用一個 Write 方法。接口越大抽象越弱,所以絕大多時候最好使用行爲而不是具體的類型。
- 糟糕的結構體字段排序
糟糕順序的結構體雖然也不會導致任何錯誤,但是它會造成更多的內存消耗。
type BadOrderedPerson struct {
Veteran bool // 1 byte
Name string // 16 byte
Age int32 // 4 byte
}
type OrderedPerson struct {
Name string
Age int32
Veteran bool
}
上面代碼看起來兩種類型都佔用了相同的 21bytes 的內存空間,但是結果顯示卻完全不同。我們使用 GOARCH=amd64 來編譯代碼:
-
BadOrderedPerson 類型分配了 32bytes
-
OrderedPerson 類型分配了 24bytes
爲什麼會這樣呢?原因是數據結構對齊。在 64 位架構中,內存分配 8 字節的連續數據包。需要添加的填充可以通過下面的公式計算得出:
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{}
}
當我們高頻使用一個大的糟糕排序的結構體類型,會導致性能問題。但是不用擔心,我們不用人肉檢查結構體順序定義問題,使用 maligned(https://github.com/mdempsky/maligned) 可以輕鬆檢查此類問題。
- 測試中不使用 race detector
數據競爭會引發神祕的錯誤,經常發生在我們代碼部署線上部署很長一段時間後。正是這個原因,它也是併發系統中最常見也是最難調試的問題。爲了幫助區分這類 bug,Go1.1 引入了一個內置的數據競爭檢測器。使用過程只需要簡單的添加一個 -race 標誌即可。
$ 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
啓用 race 後,編譯器會記錄代碼訪問內存的時間和方式,而 runtime 監視共享變量的非同步訪問。
當數據競爭被檢測到,競爭檢測器會打印一份報告,包括衝突訪問的堆棧跟蹤信息。一下是一個栗子:
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
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/A2KR5x3lbum0OG0ST-FrZQ