<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Go避坑指南:這些錯誤你犯過嗎?

          共 9025字,需瀏覽 19分鐘

           ·

          2021-06-03 00:14

          點擊上方藍色“Go語言中文網(wǎng)”關(guān)注,每天一起學(xué) 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{123}

          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{123}

          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{123}

          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)該想辦法探究出原因,知道為什么,下次再掉坑的可能性就會小很多。

          除了以上這幾點,還有哪些你常碰到的錯誤或坑呢?歡迎留言交流!



          推薦閱讀


          福利

          我為大家整理了一份從入門到進階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進階看什么。關(guān)注公眾號 「polarisxu」,回復(fù) ebook 獲??;還可以回復(fù)「進群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。

          瀏覽 35
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  另类图片亚洲色图 | 天天综合日本网 | 蜜桃无码久久久久 | 青草草视频精品视频免费观看 | 五月花婷婷|