告誡 Go 初學者,千萬別掉進這 5 個坑

初學 golang 我們經常會犯一些錯誤,雖然它們不會產生類型檢查的異常,但是它們往往潛在影響軟件的功能。

  1. 循環中易犯的錯誤

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 不能容忍愚蠢的人。

  1. 發送到一個無保證的 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 將永遠阻塞。

解決方案:

  1. 不使用接口

接口的使用可以讓我們的代碼更加靈活,也是一種在代碼中引入多態的方法。接口允許我們請求一組行爲而不是特定類型。不使用接口不會產生任何錯誤,但是它會導致我們的代碼不簡潔、不靈活、並且不具備可拓展性。

衆多接口中,io.Readerio.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 方法。接口越大抽象越弱,所以絕大多時候最好使用行爲而不是具體的類型。

  1. 糟糕的結構體字段排序

糟糕順序的結構體雖然也不會導致任何錯誤,但是它會造成更多的內存消耗。

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 來編譯代碼:

爲什麼會這樣呢?原因是數據結構對齊。在 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) 可以輕鬆檢查此類問題。

  1. 測試中不使用 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