一、協(xié)程錯(cuò)誤管理
我們?cè)诨A(chǔ)系列講過Go程序開發(fā)中的錯(cuò)誤處理規(guī)范,展示了幾種函數(shù)執(zhí)行中的錯(cuò)誤返回問題,而在Go并發(fā)編程中,我們常常會(huì)忽略協(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ò)請(qǐng)求中返回響應(yīng),并通過一個(gè)通道把多次請(qǐng)求的響應(yīng)信息發(fā)送給父協(xié)程,但里面忽略的網(wǎng)絡(luò)請(qǐng)求有可能發(fā)生錯(cuò)誤。如果其中某個(gè)請(qǐng)求失敗,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è)鏈接請(qǐng)求得到的結(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)
// 嘗試請(qǐng)求多個(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ò)誤會(huì)提供什么。錯(cuò)誤表明您的系統(tǒng)已進(jìn)入無法完成用戶明確或隱含請(qǐng)求的操作的狀態(tài)。
因此,它需要傳遞一些關(guān)鍵信息:
發(fā)生了什么?
這是錯(cuò)誤的一部分,其中包含有關(guān)所發(fā)生事件的信息,例如“磁盤已滿”,“套接字已關(guān)閉”或“憑證過期”。盡管生成錯(cuò)誤的內(nèi)容可能會(huì)隱式生成此信息,你可以用一些能夠幫助用戶的上下文來完善它。何時(shí)何處發(fā)生?
錯(cuò)誤應(yīng)始終包含一個(gè)完整的堆棧跟蹤,從調(diào)用的啟動(dòng)方式開始,直到實(shí)例化錯(cuò)誤。
此外,錯(cuò)誤應(yīng)該包含有關(guān)它正在運(yùn)行的上下文的信息。 例如,在分布式系統(tǒng)中,它應(yīng)該有一些方法來識(shí)別發(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)槲覀兛梢允褂么罱蚣艿乃悸穪韺?duì)待錯(cuò)誤處理。
可以將所有錯(cuò)誤歸納為兩個(gè)類別:
- 程序Bug:Bug是你沒有為系統(tǒng)定制的錯(cuò)誤,或者是“原始”錯(cuò)誤。
- 已知業(yè)務(wù)及系統(tǒng)意外:例如,網(wǎng)絡(luò)連接斷開,磁盤寫入失敗等。
我們知道一個(gè)健壯的系統(tǒng)總會(huì)分層構(gòu)建,如在一個(gè)典型的web請(qǐng)求中,用戶發(fā)出請(qǐng)求,系統(tǒng)的web API接受用戶請(qǐng)求(高層),分析用戶請(qǐng)求的業(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ū)別的,對(duì)用戶呈現(xiàn)的信息要盡量友好,而對(duì)開發(fā)人員呈現(xiàn)的信息要盡量完整詳細(xì),以便開發(fā)人員排查bug。所以你應(yīng)該很容易就能想到方案:即對(duì)每一層每一種錯(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, // 存儲(chǔ)我們正在包裝的錯(cuò)誤。 如果需要調(diào)查發(fā)生的事情,我們總是希望能夠查看到最低級(jí)別的錯(cuò)誤。
Message: fmt.Sprintf(messagef, msgArgs...),
StackTrace: string(debug.Stack()), // 記錄了創(chuàng)建錯(cuò)誤時(shí)的堆棧跟蹤。
Misc: make(map[string]interface{}), // 創(chuàng)建一個(gè)雜項(xiàng)信息存儲(chǔ)字段??梢源鎯?chǔ)并發(fā)ID,堆棧跟蹤的hash或可能有助于診斷錯(cuò)誤的其他上下文信息。
}
}
我們先從低層級(jí)開始,定義一個(gè)底層級(jí)的錯(cuò)誤信息
// "lowlevel" module
type LowLevelErr struct {
error
}
func LowLevelModule(path string) (bool, error) {
info, err := os.Stat(path)
// 發(fā)生錯(cuò)誤時(shí),使用錯(cuò)誤包裝器返回一個(gè)自定義的底層級(jí)錯(cuò)誤類型,我們想隱藏工作未運(yùn)行原因的底層細(xì)節(jié),因?yàn)檫@對(duì)于用戶并不重要。
if err != nil {
return false, LowLevelErr{wrapError(err, err.Error())}
}
return info.Mode().Perm()&0100 == 0100, nil
}
接下來看看中間層,定義一個(gè)中間層級(jí)的錯(cuò)誤信息
// "intermediate" module
type IntermediateErr struct {
error
}
func IntermediateLevelModule(id string) error {
const jobBinPath = "/bad/job/binary"
// 調(diào)用底層級(jí)的函數(shù) ,接收其中的錯(cuò)誤信息
isExecutable, err := LowLevelModule(jobBinPath)
if err != nil {
// 發(fā)現(xiàn)錯(cuò)誤,使用錯(cuò)誤包裝器封裝來自上一層的錯(cuò)誤消息,并添加當(dāng)前層級(jí)的錯(cuò)誤信息
return IntermediateErr{wrapError(err,
"cannot run job %q: requisite binaries not available", id)} //1
} else if isExecutable == false {
// 由于沒有底層級(jí)的錯(cuò)誤,包裝器第一參數(shù)只需傳入nil
return wrapError(
nil,
"cannot run job %q: requisite binaries are not executable", id,
)
}
return exec.Command(jobBinPath, "--id="+id).Run()
}
ok,接下來看看高層級(jí)的調(diào)用,我們定義了一個(gè)直接對(duì)用戶呈現(xiàn)的錯(cuò)誤函數(shù),對(duì)開發(fā)人員記錄錯(cuò)誤日志,對(duì)用戶呈現(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)行包裝,并且自由度非常大。