日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

在Go中,你犯過這些錯誤嗎

 風聲之家 2021-04-28

Go語言中文網(wǎng) 今天

以下文章來源于吳親強的深夜食堂 ,作者吳親庫里

吳親強的深夜食堂

吳親強的深夜食堂

關(guān)注一些奇奇怪怪的設(shè)計,分享一些有有趣趣的生活

迭代器變量上使用 goroutine

這算高頻吧。

package main

import (
  "fmt"
  "sync"
)

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

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

圖片

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

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

正確的做法,

func main() {
  var wg sync.WaitGroup

  items := []int{12345}
  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 作為一個參數(shù)傳入閉包中,i 每次迭代都會被求值, 并放置在 goroutine 的堆棧中,因此每個切片元素最終都會被執(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使用不當,也會犯錯

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

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)問題嚴重性了嗎?

當用戶id等于9的時候,err !=nil 直接 return 了,導(dǎo)致 waitGroup 計數(shù)器根本沒機會減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() //重點

      //....業(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("處理失敗")
}

但是如果這時候我們直接開啟一個 goroutine,在這個 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)
}

此時最外層的 recover 并不能捕獲,程序會直接掛掉。 圖片

但是你總不能每次開啟一個新的 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("錯誤失敗")
  }()

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

  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("錯誤失敗")
  })

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

  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("錯誤:%v\n", err)
    }
  }()
  fn()
}

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


channel

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

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

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

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


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

package main

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


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

package main

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

另外,有時候使用 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)
}

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

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

總結(jié)

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

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多