<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中,你犯過這些錯(cuò)誤嗎

          共 8738字,需瀏覽 18分鐘

           ·

          2021-04-28 17:23

          迭代器變量上使用 goroutine

          這算高頻吧。


          package main

          import (
            "fmt"
            "sync"
          )

          func main() {
            var wg sync.WaitGroup
            items := []int{1, 2, 3, 4, 5}
            for index, _ := range items {
              wg.Add(1)
              go func() {
                defer wg.Done()
                fmt.Printf("item:%v\\n", items[index])
              }()
            }
            wg.Wait()
          }


          一個(gè)很簡單的利用 sync.waitGroup 做任務(wù)編排的場景,看一下好像沒啥問題,運(yùn)行看看結(jié)果。

          為啥不是1-5(當(dāng)然不是順序的)。


          原因很簡單,循環(huán)器中的 i 實(shí)際上是一個(gè)單變量,go func 里的閉包只綁定在一個(gè)變量上, 每個(gè) goroutine 可能要等到循環(huán)結(jié)束才真正的運(yùn)行,這時(shí)候運(yùn)行的 i 值大概率就是5了。沒人能保證這個(gè)過程,有的只是手段。

          正確的做法,


          func main() {
            var wg sync.WaitGroup

            items := []int{1, 2, 3, 4, 5}
            for index, _ := range items {
              wg.Add(1)
              go func(i int) {
                defer wg.Done()
                fmt.Printf("item:%v\\n", items[i])
              }(index)
            }
            wg.Wait()
          }

          通過將 i 作為一個(gè)參數(shù)傳入閉包中,i 每次迭代都會被求值, 并放置在 goroutine 的堆棧中,因此每個(gè)切片元素最終都會被執(zhí)行打印。

          或者這樣,


          for index, _ := range items {
              wg.Add(1)
              i:=index
              go func() {
                defer wg.Done()
                fmt.Printf("item:%v\\n", items[i])
              }()
            }


          WaitGroup

          上面的例子有用到 sync.waitGroup,使用不當(dāng),也會犯錯(cuò)。

          我把上面的例子稍微改動復(fù)雜一點(diǎn)點(diǎn)。


          package main

          import (
            "errors"
            "github.com/prometheus/common/log"
            "sync"
          )

          type User struct {
            userId int
          }

          func main() {
            var userList []User
            for i := 0; i < 10; i++ {
              userList = append(userList, User{userId: i})
            }

            var wg sync.WaitGroup
            for i, _ := range userList {
              wg.Add(1)
              go func(item int) {
                _, err := Do(userList[item])
                if err != nil {
                  log.Infof("err message:%v\\n", err)
                  return
                }
                wg.Done()
              }(i)
            }
            wg.Wait()

            // 處理其他事務(wù)
          }

          func Do(user User) (string, error) {
            // 處理雜七雜八的業(yè)務(wù)....
            if user.userId == 9 {
              // 此人是非法用戶
              return "失敗", errors.New("非法用戶")
            }
            return "成功", nil
          }


          發(fā)現(xiàn)問題嚴(yán)重性了嗎?

          當(dāng)用戶id等于9的時(shí)候,err !=nil 直接 return 了,導(dǎo)致 waitGroup 計(jì)數(shù)器根本沒機(jī)會減1, 最終 wait 會阻塞,多么可怕的 bug。

          在絕大多數(shù)的場景下,我們都必須這樣:


          func main() {
            var userList []User
            for i := 0; i < 10; i++ {
              userList = append(userList, User{userId: i})
            }
            var wg sync.WaitGroup
            for i, _ := range userList {
              wg.Add(1)
              go func(item int) {
                defer wg.Done() //重點(diǎn)

                //....業(yè)務(wù)代碼
                //....業(yè)務(wù)代碼
                _, err := Do(userList[item])
                if err != nil {
                  log.Infof("err message:%v\n", err)
                  return
                }
              }(i)
            }
            wg.Wait()
          }


          野生 goroutine

          我不知道你們公司是咋么處理異步操作的,是下面這樣嗎?


          func main() {
            // doSomething
            go func() {
              // doSomething
            }()
          }

          我們?yōu)榱朔乐钩绦蛑谐霈F(xiàn)不可預(yù)知的 panic,導(dǎo)致程序直接掛掉,都會加入 recover,

          func main() {
            defer func() {
              if err := recover(); err != nil {
                fmt.Printf("%v\n", err)
              }
            }()
            panic("處理失敗")
          }

          但是如果這時(shí)候我們直接開啟一個(gè) goroutine在這個(gè) goroutine 里面發(fā)生了 panic,

          func main() {
            defer func() {
              if err := recover(); err != nil {
                fmt.Printf("%v\n", err)
              }
            }()
            go func() {
              panic("處理失敗")
            }()

            time.Sleep(2 * time.Second)
          }

          此時(shí)最外層的 recover 并不能捕獲,程序會直接掛掉。 

          但是你總不能每次開啟一個(gè)新的 goroutine 就在里面 recover,

          func main() {
            defer func() {
              if err := recover(); err != nil {
                fmt.Printf("%v\n", err)
              }
            }()

            // func1
            go func() {
              defer func() {
                if err := recover(); err != nil {
                  fmt.Printf("%v\n", err)
                }
              }()
              panic("錯(cuò)誤失敗")
            }()

            // func2
            go func() {
              defer func() {
                if err := recover(); err != nil {
                  fmt.Printf("%v\n", err)
                }
              }()
              panic("請求錯(cuò)誤")
            }()

            time.Sleep(2 * time.Second)
          }


          多蠢啊。所以基本上大家都會包一層。


          package main

          import (
            "fmt"
            "time"
          )

          func main() {
            defer func() {
              if err := recover(); err != nil {
                fmt.Printf("%v\n", err)
              }
            }()

            // func1
            Go(func() {
              panic("錯(cuò)誤失敗")
            })

            // func2
            Go(func() {
              panic("請求錯(cuò)誤")
            })

            time.Sleep(2 * time.Second)
          }

          func Go(fn func()) {
            go RunSafe(fn)
          }

          func RunSafe(fn func()) {
            defer func() {
              if err := recover(); err != nil {
                fmt.Printf("錯(cuò)誤:%v\n", err)
              }
            }()
            fn()
          }


          當(dāng)然我這里只是簡單都打印一些日志信息,一般還會帶上堆棧都信息。


          channel

          channel  go 中的地位實(shí)在太高了,各大開源項(xiàng)目到處都是 channel 的影子, 以至于你在工業(yè)級的項(xiàng)目 issues 中搜索 channel ,能看到很多的 bug, 比如 etcd 這個(gè) issue

          一個(gè)往已關(guān)閉的 channel 中發(fā)送數(shù)據(jù)引發(fā)的 panic,等等類似場景很多。

          這個(gè)故事告訴我們,否管大不大佬,改寫的 bug 還是會寫,手動狗頭。

          channel 除了上述高頻出現(xiàn)的錯(cuò)誤,還有以下幾點(diǎn):


          直接關(guān)閉一個(gè) nil 值 channel 會引發(fā) panic

          package main

          func main() {
            var ch chan struct{}
            close(ch)
          }


          關(guān)閉一個(gè)已關(guān)閉的 channel 會引發(fā) panic。

          package main

          func main() {
            ch := make(chan struct{})
            close(ch)
            close(ch)
          }

          另外,有時(shí)候使用 channel 不小心會導(dǎo)致 goroutine 泄露,比如下面這種情況,

          package main

          import (
            "context"
            "fmt"
            "time"
          )

          func main() {
            ch := make(chan struct{})
            cx, _ := context.WithTimeout(context.Background(), time.Second)
            go func() {
              time.Sleep(2 * time.Second)
              ch <- struct{}{}
              fmt.Println("goroutine 結(jié)束")
            }()

            select {
            case <-ch:
              fmt.Println("res")
            case <-cx.Done():
              fmt.Println("timeout")
            }
            time.Sleep(5 * time.Second)
          }

          啟動一個(gè) goroutine 去處理業(yè)務(wù),業(yè)務(wù)需要執(zhí)行2秒,而我們設(shè)置的超時(shí)時(shí)間是1秒。 這就會導(dǎo)致 channel 從未被讀取, 我們知道沒有緩沖的 channel 必須等發(fā)送方和接收方都準(zhǔn)備好才能操作。 此時(shí) goroutine 會被永久阻塞在 ch <- struct{}{} 這行代碼,除非程序結(jié)束。 而這就是 goroutine 泄露。

          解決這個(gè)也很簡單,把無緩沖的 channel 改成緩沖為1。

          總結(jié)

          這篇文章主要介紹了使用 Go 在日常開發(fā)中容易犯下的錯(cuò)。 當(dāng)然還遠(yuǎn)遠(yuǎn)不止這些,你可以在下方留言中補(bǔ)充你犯過的錯(cuò)。



          推薦閱讀


          福利

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

          瀏覽 28
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  无码人在线观 | 黄色A片观看 | 五月天久久激情 | 日韩爱爱视频 | 青春草视频在线免费观看 |