Go 并發(fā)編程:錯(cuò)誤處理及錯(cuò)誤傳遞

一、協(xié)程錯(cuò)誤管理

我們在基礎(chǔ)系列講過Go程序開發(fā)中的錯(cuò)誤處理規(guī)范,展示了幾種函數(shù)執(zhí)行中的錯(cuò)誤返回問題,而在Go并發(fā)編程中啊研,我們常常會忽略協(xié)程里面的錯(cuò)誤處理問題,有時(shí)候鸥拧,我們花了很多時(shí)間思考我們的各種流程將如何共享信息和協(xié)調(diào)党远,卻忘記考慮如何優(yōu)雅地處理錯(cuò)誤。Go避開了流行的錯(cuò)誤異常模型住涉,Go認(rèn)為錯(cuò)誤處理非常重要麸锉,并且在開發(fā)程序時(shí),我們應(yīng)該像關(guān)注算法一樣關(guān)注它舆声,即錯(cuò)誤處理也是業(yè)務(wù)流程的一部分~

思考錯(cuò)誤處理時(shí)最根本的問題是花沉,“應(yīng)該由誰負(fù)責(zé)處理錯(cuò)誤?”
在某些情況下媳握,程序需要停止傳遞堆棧中的錯(cuò)誤碱屁,并將它們處理掉,這樣的操作應(yīng)該何時(shí)執(zhí)行呢蛾找?
在并發(fā)進(jìn)程中娩脾,這樣的問題變得愈發(fā)復(fù)雜。因?yàn)橐粋€(gè)并發(fā)進(jìn)程獨(dú)立于其父進(jìn)程或兄弟進(jìn)程運(yùn)行打毛,所以可能很難推斷出錯(cuò)誤是如何產(chǎn)生的柿赊。

比如如下問題

checkStatus := func(done <-chan interface{}, urls ...string, ) <-chan *http.Response {
    responses := make(chan *http.Response)
    go func() {
        defer close(responses)
        for _, url := range urls {
            resp, err := http.Get(url)
            if err != nil {
                fmt.Println(err) //1
                continue
            }
            select {
            case <-done:
                return
            case responses <- resp:
            }
        }
    }()
    return responses
}

done := make(chan interface{})
defer close(done)

urls := []string{"https://www.baidu.com", "https://badhost"}
for response := range checkStatus(done, urls...) {
    fmt.Printf("Response: %v\n", response.Status)
}

上面的程序中俩功,我們開一個(gè)協(xié)程從一個(gè)網(wǎng)絡(luò)請求中返回響應(yīng),并通過一個(gè)通道把多次請求的響應(yīng)信息發(fā)送給父協(xié)程碰声,但里面忽略的網(wǎng)絡(luò)請求有可能發(fā)生錯(cuò)誤诡蜓。如果其中某個(gè)請求失敗,response信息為nil胰挑,然而父協(xié)程并不知道發(fā)生什么蔓罚?

由此可見,即使開辟協(xié)程處理一段業(yè)務(wù)邏輯瞻颂,我們也必須考慮子協(xié)程中發(fā)生的錯(cuò)誤豺谈,并把錯(cuò)誤傳遞給父協(xié)程。典型的做法是贡这,我們需要為被調(diào)用的協(xié)程函數(shù)封裝一個(gè)Result結(jié)構(gòu)體作為返回?cái)?shù)據(jù)茬末。
例如:

type Result struct { //1
    Error    error
    Response *http.Response
}

把錯(cuò)誤處理加入返回結(jié)果,改造后我們得以加強(qiáng)程序的健壯性藕坯,我們得以直到每個(gè)鏈接請求得到的結(jié)果以及錯(cuò)誤信息团南。
具體改造如下:

checkStatus := func(done <-chan interface{}, urls ...string) <-chan Result { //2
    results := make(chan Result)
    go func() {
        defer close(results)
        for _, url := range urls {
            var result Result
            resp, err := http.Get(url)
            result = Result{Error: err, Response: resp} //3
            select {
            case <-done:
                return
            case results <- result: //4
            }
        }
    }()
    return results
}

done := make(chan interface{})
defer close(done)

// 嘗試請求多個(gè)鏈接
errCount := 0
urls := []string{"a", "https://www.baidu.com", "b", "c", "d"}
for result := range checkStatus(done, urls...) {
    if result.Error != nil {
        fmt.Printf("error: %v\n", result.Error)
        errCount++
        if errCount >= 3 {
            fmt.Println("Too many errors, breaking!")
            break
        }
        continue
    }
    fmt.Printf("Response: %v\n", result.Response.Status)
}

總之,在并發(fā)協(xié)程里炼彪,不要忽略協(xié)程內(nèi)部的錯(cuò)誤處理吐根,把結(jié)果和錯(cuò)誤信息都返回給調(diào)用者。

二辐马、并發(fā)系統(tǒng)中的錯(cuò)誤傳遞

在上面的錯(cuò)誤處理中拷橘,我們討論了如何從Go協(xié)程處理錯(cuò)誤,但我們沒有提到這些錯(cuò)誤應(yīng)該是什么樣子喜爷,或者錯(cuò)誤應(yīng)該如何流經(jīng)一個(gè)龐大而復(fù)雜的系統(tǒng)冗疮。

許多開發(fā)人員認(rèn)為錯(cuò)誤傳遞是不值得關(guān)注的,或者檩帐,至少不是首先需要關(guān)注的术幔。 Go試圖通過強(qiáng)制開發(fā)者在調(diào)用堆棧中的每一幀處理錯(cuò)誤來糾正這種不良做法。首先讓我們看看錯(cuò)誤的定義湃密。錯(cuò)誤何時(shí)發(fā)生诅挑,以及錯(cuò)誤會提供什么。錯(cuò)誤表明您的系統(tǒng)已進(jìn)入無法完成用戶明確或隱含請求的操作的狀態(tài)泛源。
因此拔妥,它需要傳遞一些關(guān)鍵信息:

  • 發(fā)生了什么?
    這是錯(cuò)誤的一部分达箍,其中包含有關(guān)所發(fā)生事件的信息没龙,例如“磁盤已滿”,“套接字已關(guān)閉”或“憑證過期”。盡管生成錯(cuò)誤的內(nèi)容可能會隱式生成此信息硬纤,你可以用一些能夠幫助用戶的上下文來完善它解滓。

  • 何時(shí)何處發(fā)生?
    錯(cuò)誤應(yīng)始終包含一個(gè)完整的堆棧跟蹤筝家,從調(diào)用的啟動(dòng)方式開始伐蒂,直到實(shí)例化錯(cuò)誤。
    此外肛鹏,錯(cuò)誤應(yīng)該包含有關(guān)它正在運(yùn)行的上下文的信息。 例如恩沛,在分布式系統(tǒng)中在扰,它應(yīng)該有一些方法來識別發(fā)生錯(cuò)誤的機(jī)器。當(dāng)試圖了解系統(tǒng)中發(fā)生的情況時(shí)雷客,這些信息將具有無法估量的價(jià)值芒珠。
    另外,錯(cuò)誤應(yīng)該包含錯(cuò)誤實(shí)例化的機(jī)器上的時(shí)間搅裙,以UTC表示皱卓。

  • 有效的信息說明?
    顯示給用戶的消息應(yīng)該進(jìn)行自定義以適合你的系統(tǒng)及其用戶部逮。它只應(yīng)包含前兩點(diǎn)的簡短和相關(guān)信息娜汁。 一個(gè)友好的信息是以人為中心的,給出一些關(guān)于這個(gè)問題的指示兄朋,并且應(yīng)該是關(guān)于一行文本掐禁。

  • 如何獲取更詳細(xì)的錯(cuò)誤信息?
    在某個(gè)時(shí)刻颅和,有人可能想詳細(xì)了解發(fā)生錯(cuò)誤時(shí)的系統(tǒng)狀態(tài)傅事。提供給用戶的錯(cuò)誤信息應(yīng)該包含一個(gè)ID,該ID可以與相應(yīng)的日志交叉引用峡扩,該日志顯示錯(cuò)誤的完整信息:發(fā)生錯(cuò)誤的時(shí)間(不是錯(cuò)誤記錄的時(shí)間)蹭越,堆棧跟蹤——包括你在代碼中自定義的信息。包含堆棧跟蹤的哈希也是有幫助的教届,以幫助在bug跟蹤器中匯總類似的問題响鹃。

默認(rèn)情況下,沒有人工干預(yù)巍佑,錯(cuò)誤所能提供的信息少得可憐茴迁。 因此,我們可以認(rèn)為:在沒有詳細(xì)信息的情況下傳播給用戶任何錯(cuò)誤的行為都是錯(cuò)誤的萤衰。因?yàn)槲覀兛梢允褂么罱蚣艿乃悸穪韺Υe(cuò)誤處理堕义。
可以將所有錯(cuò)誤歸納為兩個(gè)類別:

  • 程序Bug:Bug是你沒有為系統(tǒng)定制的錯(cuò)誤,或者是“原始”錯(cuò)誤洒擦。
  • 已知業(yè)務(wù)及系統(tǒng)意外:例如怕膛,網(wǎng)絡(luò)連接斷開褐捻,磁盤寫入失敗等。

我們知道一個(gè)健壯的系統(tǒng)總會分層構(gòu)建昧狮,如在一個(gè)典型的web請求中逗鸣,用戶發(fā)出請求绰精,系統(tǒng)的web API接受用戶請求(高層)笨使,分析用戶請求的業(yè)務(wù)模塊硫椰,調(diào)用業(yè)務(wù)邏輯層處理用戶邏輯(中間層)最爬,最后調(diào)用數(shù)據(jù)訪問層操作用戶持久數(shù)據(jù)(底層)。我們看到在系統(tǒng)的高中低分層中分別承當(dāng)各自的業(yè)務(wù)計(jì)算功能爱致,其中每一層都有可能出現(xiàn)錯(cuò)誤。如果最底層出現(xiàn)錯(cuò)誤帮坚,我們需要把錯(cuò)誤向上傳遞互艾,但最終呈現(xiàn)給用戶的是什么呢?要知道阅悍,呈現(xiàn)給用戶和開發(fā)人員的信息是很大區(qū)別的,對用戶呈現(xiàn)的信息要盡量友好拳锚,而對開發(fā)人員呈現(xiàn)的信息要盡量完整詳細(xì)霍掺,以便開發(fā)人員排查bug杆烁。所以你應(yīng)該很容易就能想到方案:即對每一層每一種錯(cuò)誤類別盡可能地自定義连躏,并在錯(cuò)誤由低到高傳遞時(shí)做適當(dāng)?shù)陌b和日志記錄

好了贞滨,討論完錯(cuò)誤信息在系統(tǒng)中傳遞應(yīng)該具備什么要素后晓铆,我們來封裝一個(gè)簡單的錯(cuò)誤實(shí)例:

// 自定義一個(gè)簡單的錯(cuò)誤信息結(jié)構(gòu)體骄噪,其實(shí)現(xiàn)了go基本錯(cuò)誤接口
type MyError struct {
    Inner      error
    Message    string
    StackTrace string
    Misc       map[string]interface{}
}
func (err MyError) Error() string {
    return err.Message
}

// 工具函數(shù):錯(cuò)誤信息在系統(tǒng)各模塊傳遞時(shí)的“錯(cuò)誤包裝器”
func wrapError(err error, messagef string, msgArgs ...interface{}) MyError {
    return MyError{
        Inner:      err, // 存儲我們正在包裝的錯(cuò)誤链蕊。 如果需要調(diào)查發(fā)生的事情谬泌,我們總是希望能夠查看到最低級別的錯(cuò)誤滔韵。
        Message:    fmt.Sprintf(messagef, msgArgs...),
        StackTrace: string(debug.Stack()),        // 記錄了創(chuàng)建錯(cuò)誤時(shí)的堆棧跟蹤。
        Misc:       make(map[string]interface{}), // 創(chuàng)建一個(gè)雜項(xiàng)信息存儲字段陪蜻。可以存儲并發(fā)ID贱鼻,堆棧跟蹤的hash或可能有助于診斷錯(cuò)誤的其他上下文信息。
    }
}

我們先從低層級開始邻悬,定義一個(gè)底層級的錯(cuò)誤信息

// "lowlevel" module
type LowLevelErr struct {
    error
}

func LowLevelModule(path string) (bool, error) {
    info, err := os.Stat(path)
    // 發(fā)生錯(cuò)誤時(shí)症昏,使用錯(cuò)誤包裝器返回一個(gè)自定義的底層級錯(cuò)誤類型父丰,我們想隱藏工作未運(yùn)行原因的底層細(xì)節(jié)肝谭,因?yàn)檫@對于用戶并不重要。
    if err != nil {
        return false, LowLevelErr{wrapError(err, err.Error())} 
    }
    return info.Mode().Perm()&0100 == 0100, nil
}

接下來看看中間層,定義一個(gè)中間層級的錯(cuò)誤信息

// "intermediate" module

type IntermediateErr struct {
    error
}

func IntermediateLevelModule(id string) error {
    const jobBinPath = "/bad/job/binary"
    // 調(diào)用底層級的函數(shù) ,接收其中的錯(cuò)誤信息
    isExecutable, err := LowLevelModule(jobBinPath)
    if err != nil {
       // 發(fā)現(xiàn)錯(cuò)誤分苇,使用錯(cuò)誤包裝器封裝來自上一層的錯(cuò)誤消息,并添加當(dāng)前層級的錯(cuò)誤信息
        return IntermediateErr{wrapError(err,
            "cannot run job %q: requisite binaries not available", id)} //1
    } else if isExecutable == false {
        // 由于沒有底層級的錯(cuò)誤医寿,包裝器第一參數(shù)只需傳入nil
        return wrapError(
            nil,
            "cannot run job %q: requisite binaries are not executable", id,
        )
    }

    return exec.Command(jobBinPath, "--id="+id).Run()
}

ok,接下來看看高層級的調(diào)用靖秩,我們定義了一個(gè)直接對用戶呈現(xiàn)的錯(cuò)誤函數(shù),對開發(fā)人員記錄錯(cuò)誤日志,對用戶呈現(xiàn)直觀的錯(cuò)誤信息扩劝。

func handleError(key int, err error, message string) {
    log.SetPrefix(fmt.Sprintf("[logID: %v]: ", key))
    log.Printf("%#v", err)
    fmt.Printf("[%v] %v", key, message)
}

func main() {
    log.SetOutput(os.Stdout)
    log.SetFlags(log.Ltime | log.LUTC)

    err := IntermediateLevelModule("1")
    if err != nil {
        msg := "There was an unexpected issue; please report this as a bug."
        if _, ok := err.(IntermediateErr); ok {
            msg = err.Error()
        }
        handleError(1, err, msg)
    }
}

這種實(shí)現(xiàn)方法與標(biāo)準(zhǔn)庫的錯(cuò)誤包兼容职辅,此外你可以用你喜歡的任何方式來進(jìn)行包裝域携,并且自由度非常大簇秒。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市秀鞭,隨后出現(xiàn)的幾起案子趋观,更是在濱河造成了極大的恐慌,老刑警劉巖锋边,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件皱坛,死亡現(xiàn)場離奇詭異,居然都是意外死亡豆巨,警方通過查閱死者的電腦和手機(jī)麸恍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來搀矫,“玉大人抹沪,你說我怎么就攤上這事∪壳颍” “怎么了融欧?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長卦羡。 經(jīng)常有香客問我噪馏,道長麦到,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任欠肾,我火速辦了婚禮瓶颠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘刺桃。我一直安慰自己粹淋,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布瑟慈。 她就那樣靜靜地躺著桃移,像睡著了一般。 火紅的嫁衣襯著肌膚如雪葛碧。 梳的紋絲不亂的頭發(fā)上借杰,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天,我揣著相機(jī)與錄音进泼,去河邊找鬼蔗衡。 笑死,一個(gè)胖子當(dāng)著我的面吹牛乳绕,可吹牛的內(nèi)容都是我干的绞惦。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼刷袍,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了樊展?” 一聲冷哼從身側(cè)響起呻纹,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎专缠,沒想到半個(gè)月后雷酪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡涝婉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年哥力,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片墩弯。...
    茶點(diǎn)故事閱讀 40,137評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡吩跋,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出渔工,到底是詐尸還是另有隱情锌钮,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布引矩,位于F島的核電站梁丘,受9級特大地震影響侵浸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜氛谜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一掏觉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧值漫,春花似錦澳腹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至晚吞,卻和暖如春延旧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背槽地。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工迁沫, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人捌蚊。 一個(gè)月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓集畅,卻偏偏與公主長得像,于是被迫代替她去往敵國和親缅糟。 傳聞我的和親對象是個(gè)殘疾皇子挺智,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容