一、協(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)行包裝域携,并且自由度非常大簇秒。