Go語言中處理并發(fā)錯誤的常用方法總結(jié)
一、 panic 只會觸發(fā)當(dāng)前 goroutine 中的 defer 操作
很多開發(fā)者初次接觸 Go 時容易誤解 panic 的作用范圍。下面我們先來看一個錯誤的代碼示例:
1.1 示例代碼
package main
import (
"fmt"
"time"
)
func main() {
// 在主 goroutine 中設(shè)置 defer,用于捕獲 panic
// 注意:這個 defer 只能捕獲發(fā)生在主 goroutine 中的 panic
defer func() {
// recover() 只能捕獲當(dāng)前 goroutine 內(nèi)的 panic,
// 如果 panic 發(fā)生在其他 goroutine 中,該 defer 無法捕獲
if e := recover(); e != nil {
fmt.Println("捕獲到 panic:", e)
}
}()
// 啟動子 goroutine,演示 panic 的傳播范圍
go func() {
// 輸出提示信息,表示子 goroutine 開始執(zhí)行
fmt.Println("子 goroutine 開始")
// 主動觸發(fā) panic,注意這里的 panic 發(fā)生在子 goroutine 內(nèi),
// 因此主 goroutine 中的 defer 無法捕獲該 panic
panic("Goroutine 發(fā)生 panic")
}()
// 主 goroutine 等待一段時間,確保子 goroutine 有足夠時間執(zhí)行
time.Sleep(2 * time.Second)
// 輸出主 goroutine 結(jié)束信息
fmt.Println("主 goroutine 結(jié)束")
}
運行這段代碼,我們會發(fā)現(xiàn),會直接報錯了:
子 goroutine 開始
panic: Goroutine 發(fā)生 panic
goroutine 18 [running]:
main.main.func2()
~/golang-tutorial/tt.go:25 +0x59
created by main.main in goroutine 1
~/golang-tutorial/tt.go:20 +0x3b
exit status 2
1.2 代碼說明
- 主 goroutine 中的 defer: 主函數(shù)開始時設(shè)置了一個 defer 函數(shù),目的是在發(fā)生 panic 時捕獲并打印錯誤信息。然而,由于 recover 只能捕獲當(dāng)前 goroutine 內(nèi)的 panic,當(dāng)子 goroutine 內(nèi)發(fā)生 panic 時,這個 defer 不會生效。
- 子 goroutine 中的 panic: 在子 goroutine 中調(diào)用 panic 后,由于沒有設(shè)置獨立的 recover 邏輯,該 goroutine 會直接崩潰,panic 信息不會傳遞到主 goroutine 中。 這樣可以清楚地看到,即使主 goroutine 使用了 defer 進行錯誤捕獲,也無法捕捉到其他 goroutine 中發(fā)生的 panic。
- 延時等待: 主 goroutine 使用
time.Sleep等待一定時間,以確保子 goroutine 有機會執(zhí)行并觸發(fā) panic,從而驗證 panic 的作用范圍。
既然程序會直接崩潰,那么,如何解決這個問題呢?
1.3 正確處理
我們只需要在子 goroutine 中使用 recover 就可以了:
package main
import (
"fmt"
"time"
)
func main() {
defer func() {
if e := recover(); e != nil {
fmt.Println("捕獲到 panic:", e)
}
}()
go func() {
defer func() {
if e := recover(); e != nil {
fmt.Println("子 goroutine 捕獲到 panic:", e)
}
}()
fmt.Println("子 goroutine 開始")
panic("Goroutine 發(fā)生 panic")
}()
time.Sleep(2 * time.Second)
fmt.Println("主 goroutine 結(jié)束")
}
運行以上代碼,可以發(fā)現(xiàn),打印出的結(jié)果為:
子 goroutine 開始 子 goroutine 捕獲到 panic: Goroutine 發(fā)生 panic 主 goroutine 結(jié)束
這就說明:panic 只會觸發(fā)當(dāng)前 goroutine 內(nèi)的 defer 操作,不能跨 goroutine 捕獲或恢復(fù)其他 goroutine 中的 panic。
二、多 goroutine 中收集錯誤和結(jié)果
假設(shè)我們有個需求,需要同時使用多個 goroutine 通過 http.Get 去請求以下四個地址,其中只有 https://httpbin.org/get 能夠正常響應(yīng),其余地址均為故意寫錯的地址:
https://httpbin1.org/gethttps://httpbin.org/gethttps://httpbin2.org/gethttps://httpbin3.org/get
2.1 如何批量收集錯誤信息?
在并發(fā)請求中,可以通過錯誤通道( error channel )來收集各個 goroutine 中發(fā)生的錯誤。例如:
package main
import (
"fmt"
"net/http"
"sync"
)
func main() {
urls := []string{
"https://httpbin1.org/get",
"https://httpbin.org/get",
"https://httpbin2.org/get",
"https://httpbin3.org/get",
}
var wg sync.WaitGroup
// 創(chuàng)建一個帶緩沖的錯誤通道,大小為 URL 數(shù)量
errCh := make(chan error, len(urls))
// 遍歷所有 URL,分別啟動 goroutine 發(fā)起請求
for _, url := range urls {
wg.Add(1)
go func(url string) {
defer wg.Done() // 保證 goroutine 結(jié)束時減少計數(shù)
resp, err := http.Get(url)
if err != nil {
// 如果請求出錯,將錯誤發(fā)送到錯誤通道中
errCh <- fmt.Errorf("請求 %s 失?。?%v", url, err)
return
}
defer resp.Body.Close()
// 打印成功信息
fmt.Printf("請求 %s 成功,狀態(tài)碼: %d\n", url, resp.StatusCode)
}(url)
}
// 等待所有 goroutine 執(zhí)行完畢
wg.Wait()
// 關(guān)閉錯誤通道
close(errCh)
// 遍歷錯誤通道,輸出所有錯誤信息
for err := range errCh {
fmt.Println("錯誤信息:", err)
}
}
在這個示例中,我們通過一個 channel errCh 來存儲每個 goroutine 產(chǎn)生的錯誤,待所有 goroutine 執(zhí)行完畢后,再統(tǒng)一處理錯誤信息。
2.2 那如果也需要結(jié)果呢?
如果希望每個請求的結(jié)果和可能的錯誤信息,我們可以定義一個結(jié)構(gòu)體,將請求的結(jié)果與錯誤信息封裝在一起,再通過 channel 收集:
package main
import (
"fmt"
"io"
"net/http"
"sync"
)
// Result 用于封裝每個請求的結(jié)果和錯誤信息
type Result struct {
URL string // 請求的 URL
StatusCode int // 返回的 HTTP 狀態(tài)碼
Err error // 請求過程中發(fā)生的錯誤
Content []byte // 返回的內(nèi)容
}
func main() {
urls := []string{
"https://httpbin1.org/get",
"https://httpbin.org/get",
"https://httpbin2.org/get",
"https://httpbin3.org/get",
}
var wg sync.WaitGroup
// 創(chuàng)建帶緩沖的結(jié)果通道,大小為 URL 數(shù)量
resCh := make(chan Result, len(urls))
// 遍歷 URL,啟動 goroutine 進行請求
for _, url := range urls {
wg.Add(1)
go func(url string) {
defer wg.Done()
resp, err := http.Get(url)
result := Result{URL: url}
if err != nil {
// 將錯誤結(jié)果封裝后發(fā)送到結(jié)果通道
result.Err = err
} else {
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
// 將成功的結(jié)果封裝后發(fā)送到結(jié)果通道
result.StatusCode = resp.StatusCode
result.Content = body
}
resCh <- result
}(url)
}
// 等待所有 goroutine 執(zhí)行完畢
wg.Wait()
close(resCh)
// 遍歷結(jié)果通道,輸出每個請求的結(jié)果和錯誤信息
for res := range resCh {
if res.Err != nil {
fmt.Printf("請求 %s 失敗: %v\n", res.URL, res.Err)
} else {
fmt.Printf("請求 %s 成功,狀態(tài)碼: %d, 內(nèi)容: %s \n", res.URL, res.StatusCode, string(res.Content))
}
}
}
在這個示例中,每個 goroutine 都會將自己的請求結(jié)果封裝到 Result 結(jié)構(gòu)體中,通過通道傳遞回來,最后我們可以一一對應(yīng)地輸出結(jié)果和錯誤信息。
三、 errgroup 包
3.1 errgroup 包簡介
golang.org/x/sync/errgroup 包提供了一個便捷的方式來管理一組 goroutine,并能統(tǒng)一收集它們產(chǎn)生的錯誤。該包的主要功能有:
- 錯誤收集與聚合: 當(dāng)多個 goroutine 發(fā)生錯誤時,errgroup 會返回第一個遇到的錯誤。
- 自動等待: 調(diào)用
g.Wait()可以等待所有啟動的 goroutine 執(zhí)行完畢。 - 與 context 結(jié)合: 通過
WithContext方法,可以為所有 goroutine 傳入相同的 context,從而實現(xiàn)統(tǒng)一的取消邏輯。
這些特性使得 errgroup 在需要并發(fā)執(zhí)行多個任務(wù)且統(tǒng)一管理錯誤時非常有用。
3.2 用 errgroup 包實戰(zhàn)一下
以下示例演示了如何使用 errgroup 包來并發(fā)請求多個 URL:
package main
import (
"fmt"
"net/http"
"golang.org/x/sync/errgroup"
)
func main() {
urls := []string{
"https://httpbin1.org/get",
"https://httpbin.org/get",
"https://httpbin2.org/get",
"https://httpbin3.org/get",
}
// 定義一個存儲結(jié)果的切片,與 errgroup 共同使用
results := make([]string, len(urls))
var g errgroup.Group
// 遍歷所有 URL,啟動 goroutine 執(zhí)行 HTTP 請求
for i, url := range urls {
i, url := i, url // 為了避免閉包引用同一個變量
g.Go(func() error {
fmt.Println("開始請求:", url)
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("請求 %s 失敗: %v", url, err)
}
defer resp.Body.Close()
results[i] = fmt.Sprintf("請求 %s 成功,狀態(tài)碼: %d", url, resp.StatusCode)
return nil
})
}
// 等待所有 goroutine 執(zhí)行完畢
if err := g.Wait(); err != nil {
fmt.Println("發(fā)生錯誤:", err)
}
// 輸出所有請求成功的結(jié)果
for _, res := range results {
fmt.Println(res)
}
}
通過運行上面的代碼,可能會打印出類似以下內(nèi)容:
開始請求: https://httpbin3.org/get 開始請求: https://httpbin2.org/get 開始請求: https://httpbin1.org/get 開始請求: https://httpbin.org/get 發(fā)生錯誤: 請求 https://httpbin3.org/get 失?。?Get "https://httpbin3.org/get": dial tcp: lookup httpbin3.org: no such host 請求 https://httpbin.org/get 成功,狀態(tài)碼: 200
我們可以得出以下重要的結(jié)論:Wait 會阻塞直至由上述 Go 方法調(diào)用的所有函數(shù)都返回,但是,如果有錯誤的話,只會記錄第一個非 nil 的錯誤,也就是說,如果有多個錯誤的情況下,不會收集所有的錯誤。
并且,通過源碼得知:當(dāng)遇到第一個錯誤時,如果之前設(shè)定了 cancel 方法,那么還會調(diào)用 cancel 方法,那么,如何創(chuàng)建帶有 cancel 方法的 errgroup.Group 呢?
3.3 使用 errgroup 包中的 WithContext 方法
有時我們希望在某個 goroutine 發(fā)生錯誤時,能夠通知其他正在執(zhí)行的任務(wù)提前取消。這時可以使用 errgroup.WithContext 方法。以下示例展示了如何實現(xiàn)這一點:
package main
import (
"context"
"fmt"
"net/http"
"golang.org/x/sync/errgroup"
)
func main() {
urls := []string{
"https://httpbin1.org/get",
"https://httpbin.org/get",
"https://httpbin2.org/get",
"https://httpbin3.org/get",
}
// 使用 context.Background 創(chuàng)建基本上下文,并通過 WithContext 包裝 errgroup
ctx := context.Background()
g, ctx := errgroup.WithContext(ctx)
// 定義存儲結(jié)果的切片
results := make([]string, len(urls))
// 遍歷所有 URL,啟動 goroutine 發(fā)起請求
for i, url := range urls {
i, url := i, url
g.Go(func() error {
fmt.Println("開始請求:", url)
// 在發(fā)起請求前,根據(jù) context 判斷是否取消
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("請求 %s 失敗: %v", url, err)
}
defer resp.Body.Close()
results[i] = fmt.Sprintf("請求 %s 成功,狀態(tài)碼: %d", url, resp.StatusCode)
return nil
})
}
// 如果有任一任務(wù)返回錯誤,將自動取消所有依賴于 ctx 的請求
if err := g.Wait(); err != nil {
fmt.Println("錯誤發(fā)生:", err)
}
for _, res := range results {
fmt.Println(res)
}
}
運行以上的代碼,打印結(jié)果如下:
開始請求: https://httpbin3.org/get 開始請求: https://httpbin.org/get 開始請求: https://httpbin2.org/get 開始請求: https://httpbin1.org/get 錯誤發(fā)生: 請求 https://httpbin1.org/get 失?。?Get "https://httpbin1.org/get": dial tcp: lookup httpbin1.org: no such host
在這個示例中,我們使用 errgroup.WithContext 創(chuàng)建了一個共享的上下文 ctx,所有的 HTTP 請求都與此 context 綁定。一旦某個請求發(fā)生錯誤并返回,其他 goroutine 中綁定該 context 的請求會立即收到取消信號,從而實現(xiàn)整體任務(wù)的協(xié)同取消。
四、總結(jié)
本文從以下幾個方面詳細(xì)介紹了在 Go 語言中如何處理并發(fā)錯誤:
- panic 和 defer: 通過示例說明 panic 只會觸發(fā)當(dāng)前 goroutine 內(nèi)的 defer 操作,并展示了即使主 goroutine 設(shè)置了 defer,也無法捕獲子 goroutine 內(nèi)的 panic。
- 并發(fā)中錯誤收集: 通過簡單示例展示了如何在多個 goroutine 中分別收集錯誤信息,以及如何關(guān)聯(lián)請求結(jié)果與錯誤信息。
- errgroup 包的使用: 介紹了 errgroup 包的核心功能,展示了如何用 errgroup 包簡化并發(fā)錯誤處理,同時詳細(xì)演示了 WithContext 方法的使用場景和效果。
通過這些示例和詳細(xì)解釋,希望大家在實際開發(fā)中能夠更加自信地處理并發(fā)任務(wù)中的錯誤問題,從而編寫出更加健壯和易維護的代碼。
以上就是Go語言中處理并發(fā)錯誤的常用方法總結(jié)的詳細(xì)內(nèi)容,更多關(guān)于Go并發(fā)錯誤處理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
以alpine作為基礎(chǔ)鏡像構(gòu)建Golang可執(zhí)行程序操作
這篇文章主要介紹了以alpine作為基礎(chǔ)鏡像構(gòu)建Golang可執(zhí)行程序操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12
利用Go實現(xiàn)一個簡易DAG服務(wù)的示例代碼
DAG的全稱是Directed Acyclic Graph,即有向無環(huán)圖,DAG廣泛應(yīng)用于表示具有方向性依賴關(guān)系的數(shù)據(jù),如任務(wù)調(diào)度、數(shù)據(jù)處理流程、項目管理以及許多其他領(lǐng)域,下面,我將用Go語言示范如何實現(xiàn)一個簡單的DAG服務(wù),需要的朋友可以參考下2024-03-03
golang服務(wù)報錯:?write:?broken?pipe的解決方案
在開發(fā)在線客服系統(tǒng)的時候,看到日志里有一些錯誤信息,下面這篇文章主要給大家介紹了關(guān)于golang服務(wù)報錯:?write:?broken?pipe的解決方案,需要的朋友可以參考下2022-09-09
Go語言實現(xiàn)單端口轉(zhuǎn)發(fā)到多個端口
這篇文章主要為大家詳細(xì)介紹了Go語言實現(xiàn)單端口轉(zhuǎn)發(fā)到多個端口,文中的示例代碼講解詳細(xì),具有一定的參考價值,對大家的學(xué)習(xí)或工作有一定的幫助,需要的小伙伴可以了解下2024-02-02

