迭代器變量上使用 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 每次迭代都會(huì)被求值, 并放置在 goroutine
的堆棧中,因此每個(gè)切片元素最終都會(huì)被執(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),也會(huì)犯錯(cuò)。
我把上面的例子稍微改動(dòng)復(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ī)會(huì)減1, 最終 wait
會(huì)阻塞,多么可怕的 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)致程序直接掛掉,都會(huì)加入 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
并不能捕獲,程序會(huì)直接掛掉。 
但是你總不能每次開啟一個(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)
}
多蠢啊。所以基本上大家都會(huì)包一層。
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)然我這里只是簡單都打印一些日志信息,一般還會(huì)帶上堆棧都信息。
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
還是會(huì)寫,手動(dòng)狗頭。
channel
除了上述高頻出現(xiàn)的錯(cuò)誤,還有以下幾點(diǎn):
直接關(guān)閉一個(gè) nil 值 channel 會(huì)引發(fā) panic
package main
func main() {
var ch chan struct{}
close(ch)
}
關(guān)閉一個(gè)已關(guān)閉的 channel 會(huì)引發(fā) panic。
package main
func main() {
ch := make(chan struct{})
close(ch)
close(ch)
}
另外,有時(shí)候使用 channel
不小心會(huì)導(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)
}
啟動(dòng)一個(gè) goroutine
去處理業(yè)務(wù),業(yè)務(wù)需要執(zhí)行2秒,而我們設(shè)置的超時(shí)時(shí)間是1秒。 這就會(huì)導(dǎo)致 channel
從未被讀取, 我們知道沒有緩沖的 channel
必須等發(fā)送方和接收方都準(zhǔn)備好才能操作。 此時(shí) goroutine
會(huì)被永久阻塞在 ch <- struct{}{}
這行代碼,除非程序結(jié)束。 而這就是 goroutine
泄露。
解決這個(gè)也很簡單,把無緩沖的 channel
改成緩沖為1。
總結(jié)
這篇文章主要介紹了使用 Go
在日常開發(fā)中容易犯下的錯(cuò)。 當(dāng)然還遠(yuǎn)遠(yuǎn)不止這些,你可以在下方留言中補(bǔ)充你犯過的錯(cuò)。