golang中channel+error來(lái)做異步錯(cuò)誤處理有多香
官方推薦golang中錯(cuò)誤處理當(dāng)做值處理, 既然是值那就可以在channel中傳輸,本文帶你看看golang中channel+error來(lái)做異步錯(cuò)誤處理有多香,看完本文還會(huì)覺得golang的錯(cuò)誤處理相比java try catch一點(diǎn)優(yōu)勢(shì)都沒有嗎?
場(chǎng)景
如下,一次任務(wù)起多個(gè)協(xié)程異步處理任務(wù),比如同時(shí)做服務(wù)/redis/mysql/kafka初始化,當(dāng)某一個(gè)協(xié)程出現(xiàn)錯(cuò)誤(初始化失敗)時(shí),程序是停止還是繼續(xù)呢?如何記錄錯(cuò)誤?如何控制優(yōu)雅的退出全部工作協(xié)程呢?

為了解決類似的問題,常見如下三種解決方案:
1.中斷退出并記錄日志
如下,最簡(jiǎn)單粗暴的方式就是,一旦協(xié)程中發(fā)生錯(cuò)誤,記錄日志立即退出,外層如果想取到錯(cuò)誤可以通過共享全局變量,單個(gè)協(xié)程無(wú)法控制所有協(xié)程動(dòng)作。
// 出錯(cuò)中斷協(xié)程,打印日志,退出
func TestSimpleExit(t *testing.T) {
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
//do something
if err := doSomething(); err != nil {
t.Logf("Error when call doSomething:%v\n", err)
return
}
}()
wg.Wait()
}2.監(jiān)控error,可選記錄日志或退出
前面講過,既然error是值,那就可以在channel中傳輸,可以單獨(dú)開一個(gè)channel,所有協(xié)程錯(cuò)誤都發(fā)送到這個(gè)通道。
// 數(shù)據(jù)處理流程
dataFunc := func(ctx context.Context, dataChan chan int, errChan chan error) {
defer wg.Done()
for {
select {
case v, ok := <-dataChan:
if !ok {
log.Println("Receive data channel close msg!")
return
}
if err := doSomething2(v); err != nil {
errChan <- err
continue
}
// do ...
case <-ctx.Done():
log.Println("Receive exit msg!")
return
}
}
}
wg.Add(1)
go dataFunc(ctx, dataChan, errChan)
wg.Add(1)
go dataFunc(ctx, dataChan, errChan)
監(jiān)控錯(cuò)誤error通道,統(tǒng)一記錄和退出,一旦檢測(cè)到錯(cuò)誤可以通過ctx通知所有協(xié)程退出,這里可以靈活控制監(jiān)控到錯(cuò)誤時(shí)的錯(cuò)誤處理策略(是否記錄日志/是否退出等),error通道可以同步或異步處理。
整體流程如下

異步監(jiān)控error
// 錯(cuò)誤處理流程,error處理通道異步等待
wg.Add(1)
go func(errChan chan error) {
defer wg.Done()
for {
select {
case v, ok := <-errChan:
if !ok {
log.Println("Receice err channel close msg!")
return
}
// 收到錯(cuò)誤時(shí),可選擇記錄日志或退出
if v != nil {
t.Logf("Error when call doSomething:%v\n", v)
cancel() // 通知全部退出
return
}
case <-ctx.Done():
log.Println("Receive exit msg!")
return
}
}
}(errChan)
dataChan <- 1
wg.Wait()同步監(jiān)控error
// 錯(cuò)誤處理流程,error處理通道同步等待
for {
select {
case v, ok := <-errChan:
if !ok {
log.Println("Receice err channel close msg!")
goto EXIT
}
// 收到錯(cuò)誤時(shí),可選擇記錄日志或退出
if v != nil {
t.Logf("Error when call doSomething:%v\n", v)
cancel()
goto EXIT
}
case <-ctx.Done():
log.Println("Receive exit msg!")
goto EXIT
}
}
EXIT:
wg.Wait()3.官方庫(kù)errgroup
考慮到error輸出到通道后統(tǒng)一處理是golang常用手段,官方也針對(duì)封裝了一個(gè)error處理包,errgroup顧名思義,多個(gè)協(xié)程的error被當(dāng)做一個(gè)組,一旦某個(gè)協(xié)程出錯(cuò)所有協(xié)程都退出,只輸出第一個(gè)error。
func TestSimpleChannel5(t *testing.T) {
eg, ctx := errgroup.WithContext(context.Background())
dataChan := make(chan int)
defer close(dataChan)
// 數(shù)據(jù)處理流程
dataFunc := func() error {
for {
select {
case v, ok := <-dataChan:
if !ok {
log.Println("Receive data channel close msg!")
return nil
}
if err := doSomething2(v); err != nil {
return err
}
// do ...
// 增加ctx通知完成
case <-ctx.Done():
log.Println("Receive exit msg!")
return nil
}
}
}
eg.Go(dataFunc)
eg.Go(dataFunc)
eg.Go(dataFunc)
dataChan <- 1
// 錯(cuò)誤處理流程,任何一個(gè)協(xié)程出現(xiàn)error,則會(huì)調(diào)用ctx對(duì)應(yīng)cancel函數(shù),所有相關(guān)協(xié)程都會(huì)退出
if err := eg.Wait(); err != nil {
fmt.Printf("Something is wrong->%v\n", err)
}
}
類似上一小節(jié),可以看到errgroup就是結(jié)合waitgroup cancel和channel通道封裝的。
4.監(jiān)控error,全部日志合并后輸出
同樣是上述場(chǎng)景,有時(shí)候我們的需求是返回所有的錯(cuò)誤(不是第一個(gè)錯(cuò)誤)。
- 比如在服務(wù)啟動(dòng)時(shí),對(duì) redis、kafka、mysql 等各種資源初始化場(chǎng)景,可以把所有相關(guān)資源初始化的錯(cuò)誤都返回,展示給用戶統(tǒng)一排查。
- 另一種場(chǎng)景就是在 web 請(qǐng)求中,校驗(yàn)請(qǐng)求參數(shù)時(shí),返回所有參數(shù)的校驗(yàn)錯(cuò)誤給客戶端的場(chǎng)景。
這種需求,一般考慮使用多錯(cuò)誤管理(hashicorp/go-multierror庫(kù)),如下一個(gè)簡(jiǎn)答同步任務(wù)演示多錯(cuò)誤管理,所有返回的錯(cuò)誤可以通過Append歸并成一個(gè)錯(cuò)誤,實(shí)際上是通過error wrap的方式合并起來(lái)的,因此也可以使用Is/As判斷嵌套error。
// 多路協(xié)程error合并,用于多路check場(chǎng)景
func TestSimpleChannel3(t *testing.T) {
// 同步執(zhí)行多個(gè)任務(wù),返回error合并
var err = func() error {
var result error
if err := doSomething(); err != nil {
result = multierror.Append(result, err)
}
if err := doSomething2(nil); err != nil {
result = multierror.Append(result, err)
}
return result
}()
// 打印輸出
if err != nil {
fmt.Printf("%v\n", err)
}
// 獲取錯(cuò)誤列表
if err != nil {
if merr, ok := err.(*multierror.Error); ok {
fmt.Printf("%v\n", merr.Errors)
}
}
// 判斷是否為某種類型
if err != nil && errors.Is(err, Error1) {
fmt.Println("Errors contain error 1")
}
// 判斷是否其中一個(gè)error能夠轉(zhuǎn)換成指定error
var e MyError
if err != nil && errors.As(err, &e) {
fmt.Println("One Error can be convert to nyerror")
}
}
那么,在起多個(gè)異步任務(wù)時(shí),就可以如下處理,返回的多個(gè)error通過channel消費(fèi)合并展示。
func TestSimpleChannel4(t *testing.T) {
wg := sync.WaitGroup{}
taskNum := 10
errChan := make(chan error, taskNum)
// 異步執(zhí)行多個(gè)任務(wù)
step := func(stepNum int, errChan chan error) {
defer wg.Done()
errChan <- fmt.Errorf("step %d error", stepNum)
}
for i := 0; i < taskNum; i++ {
wg.Add(1)
go step(i, errChan)
}
// 等待任務(wù)完成
go func() {
wg.Wait()
close(errChan)
}()
// err通道阻塞等待,可能的所有錯(cuò)誤合并
var result *multierror.Error
for err := range errChan {
result = multierror.Append(result, err)
}
// 出現(xiàn)一個(gè)錯(cuò)誤時(shí),選擇記錄日志或退出
if len(result.Errors) != 0 {
log.Println(result.Errors)
}
}
參考文獻(xiàn)
演示代碼 https://gitee.com/wenzhou1219/go-in-prod/tree/master/error_group
errgroup源碼解析 https://zhuanlan.zhihu.com/p/416054707
errgroup使用參考 https://zhuanlan.zhihu.com/p/338999914
go-multierror使用參考 https://zhuanlan.zhihu.com/p/581030231
到此這篇關(guān)于golang 錯(cuò)誤處理channel+error真的香的文章就介紹到這了,更多相關(guān)golang 錯(cuò)誤處理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用Go語(yǔ)言中的Context取消協(xié)程執(zhí)行的操作代碼
在 Go 語(yǔ)言中,協(xié)程(goroutine)是一種輕量級(jí)的線程,非常適合處理并發(fā)任務(wù),然而,如何優(yōu)雅地取消正在運(yùn)行的協(xié)程是一個(gè)常見的問題,本文將通過一個(gè)具體的例子來(lái)展示如何使用 context 包來(lái)取消協(xié)程的執(zhí)行,需要的朋友可以參考下2024-11-11
Go中字符串處理?fmt.Sprintf與string.Builder的區(qū)別對(duì)比分析
在Go語(yǔ)言中,我們通常會(huì)遇到兩種主要的方式來(lái)處理和操作字符串:使用fmt.Sprintf函數(shù)和string.Builder類型,本文給大家介紹它們?cè)谛阅芎陀梅ㄉ嫌幸恍╆P(guān)鍵區(qū)別,感興趣的朋友跟隨小編一起看看吧2023-11-11
Go語(yǔ)言實(shí)現(xiàn)牛頓法求平方根函數(shù)的案例
這篇文章主要介紹了Go語(yǔ)言實(shí)現(xiàn)牛頓法求平方根函數(shù)的案例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來(lái)看看吧2020-12-12
golang 調(diào)用c語(yǔ)言動(dòng)態(tài)庫(kù)方式實(shí)現(xiàn)
本文主要介紹了golang 調(diào)用c語(yǔ)言動(dòng)態(tài)庫(kù)方式實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12

