多 Goroutine 如何優雅處理錯誤?
大家好,我是煎魚。
在 Go 語言中,goroutine 的使用是非常頻繁的,因此在日常編碼的時候我們會遇到一個問題,那就是 goroutine 裏面的錯誤處理,怎麼做比較好?
這是來自我讀者羣的問題。作爲一個寵粉煎魚,我默默記下了這個技術話題。今天煎魚就大家來看看多 goroutine 的錯誤處理機制也有哪些!
一般來講,我們的業務代碼會是:
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
log.Println("腦子進煎魚了")
wg.Done()
}()
go func() {
log.Println("煎魚想報錯...")
wg.Done()
}()
time.Sleep(time.Second)
}
在上述代碼中,我們運行了多個 goroutine。但我想拋出 error 的錯誤信息出來,似乎沒什麼好辦法...
通過錯誤日誌記錄
爲此,業務代碼中常見的第一種方法:通過把錯誤記錄寫入日誌文件中,再結合相關的 logtail 進行採集和梳理。
但這又會引入新的問題,那就是調用錯誤日誌的方法寫的到處都是。代碼結構也比較亂,不直觀。
最重要的是無法針對 error 做特定的邏輯處理和流轉。
利用 channel 傳輸
這時候大家可能會想到 Go 的經典哲學:不要通過共享內存來通信,而是通過通信來實現內存共享(Do not communicate by sharing memory; instead, share memory by communicating)。
第二種的方法:利用 channel 來傳輸多個 goroutine 中的 errors:
func main() {
gerrors := make(chan error)
wgDone := make(chan bool)
var wg sync.WaitGroup
wg.Add(2)
go func() {
wg.Done()
}()
go func() {
err := returnError()
if err != nil {
gerrors <- err
}
wg.Done()
}()
go func() {
wg.Wait()
close(wgDone)
}()
select {
case <-wgDone:
break
case err := <-gerrors:
close(gerrors)
fmt.Println(err)
}
time.Sleep(time.Second)
}
func returnError() error {
return errors.New("煎魚報錯了...")
}
輸出結果:
煎魚報錯了...
雖然使用 channel 後已經方便了不少。但自己編寫 channel 總是需要關心一些非業務向的邏輯。
藉助 sync/errgroup
因此第三種方法,就是使用官方提供的 sync/errgroup
標準庫:
type Group
func WithContext(ctx context.Context) (*Group, context.Context)
func (g *Group) Go(f func() error)
func (g *Group) Wait() error
-
Go:啓動一個協程,在新的 goroutine 中調用給定的函數。
-
Wait:等待協程結束,直到來自 Go 方法的所有函數調用都返回,然後返回其中的第一個非零錯誤(如果有的話)。
結合其特性能夠非常便捷的針對多 goroutine 進行錯誤處理:
func main() {
g := new(errgroup.Group)
var urls = []string{
"http://www.golang.org/",
"https://golang2.eddycjy.com/",
"https://eddycjy.com/",
}
for _, url := range urls {
url := url
g.Go(func() error {
resp, err := http.Get(url)
if err == nil {
resp.Body.Close()
}
return err
})
}
if err := g.Wait(); err == nil {
fmt.Println("Successfully fetched all URLs.")
} else {
fmt.Printf("Errors: %+v", err)
}
}
在上述代碼中,其表現的是爬蟲的案例。每一個計劃新起的 goroutine 都直接使用 Group.Go
方法。在等待和錯誤上,直接調用 Group.Wait
方法就可以了。
使用標準庫 sync/errgroup
這種方法的好處就是不需要關注非業務邏輯的控制代碼,比較省心省力。
進階使用
在真實的工程代碼中,我們還可以基於 sync/errgroup
實現一個 http server 的啓動和關閉 ,以及 linux signal 信號的註冊和處理。以此保證能夠實現一個 http server 退出,全部註銷退出。
參考代碼(@via 毛老師)如下:
func main() {
g, ctx := errgroup.WithContext(context.Background())
svr := http.NewServer()
// http server
g.Go(func() error {
fmt.Println("http")
go func() {
<-ctx.Done()
fmt.Println("http ctx done")
svr.Shutdown(context.TODO())
}()
return svr.Start()
})
// signal
g.Go(func() error {
exitSignals := []os.Signal{os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT} // SIGTERM is POSIX specific
sig := make(chan os.Signal, len(exitSignals))
signal.Notify(sig, exitSignals...)
for {
fmt.Println("signal")
select {
case <-ctx.Done():
fmt.Println("signal ctx done")
return ctx.Err()
case <-sig:
// do something
return nil
}
}
})
// inject error
g.Go(func() error {
fmt.Println("inject")
time.Sleep(time.Second)
fmt.Println("inject finish")
return errors.New("inject error")
})
err := g.Wait() // first error return
fmt.Println(err)
}
內部基礎框架有非常有這種代碼,有興趣的可以自己模仿着寫一遍,收貨會很多。
總結
在 Go 語言中 goroutine 是非常常用的一種方法,爲此我們需要更瞭解 goroutine 配套的上下游(像是 context、error 處理等),應該如何用什麼來保證。
再在團隊中形成一定的共識和規範,這麼工程代碼閱讀起來就會比較的舒適,一些很坑的隱藏 BUG 也會少很多 :)
關注煎魚,吸取他的知識 👆
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/NX6kVJP-RdUzcCmG2MF31w