迭代器變量上使用 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è)很簡(jiǎn)單的利用 sync.waitGroup 做任務(wù)編排的場(chǎng)景,看一下好像沒啥問題,運(yùn)行看看結(jié)果。

為啥不是1-5(當(dāng)然不是順序的)。
原因很簡(jiǎn)單,循環(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ù)的場(chǎng)景下,我們都必須這樣:
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("請(qǐng)求錯(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("請(qǐng)求錯(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)然我這里只是簡(jiǎn)單都打印一些日志信息,一般還會(huì)帶上堆棧都信息。
channel
channel 在 go 中的地位實(shí)在太高了,各大開源項(xiàng)目到處都是 channel 的影子, 以至于你在工業(yè)級(jí)的項(xiàng)目 issues 中搜索 channel ,能看到很多的 bug, 比如 etcd 這個(gè) issue, 
一個(gè)往已關(guān)閉的 channel 中發(fā)送數(shù)據(jù)引發(fā)的 panic,等等類似場(chǎng)景很多。
這個(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秒,而我們?cè)O(shè)置的超時(shí)時(shí)間是1秒。 這就會(huì)導(dǎo)致 channel 從未被讀取, 我們知道沒有緩沖的 channel 必須等發(fā)送方和接收方都準(zhǔn)備好才能操作。 此時(shí) goroutine 會(huì)被永久阻塞在 ch <- struct{}{} 這行代碼,除非程序結(jié)束。 而這就是 goroutine 泄露。
解決這個(gè)也很簡(jiǎn)單,把無緩沖的 channel 改成緩沖為1。
總結(jié)
這篇文章主要介紹了使用 Go 在日常開發(fā)中容易犯下的錯(cuò)。 當(dāng)然還遠(yuǎn)遠(yuǎn)不止這些,你可以在下方留言中補(bǔ)充你犯過的錯(cuò)。