迭代器變量上使用 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()
}
一個很簡單的利用 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{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
作為一個參數(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ā)中容易犯下的錯。 當然還遠遠不止這些,你可以在下方留言中補充你犯過的錯。