使用并發代碼,特別是分布式系統,在系統中很容易出現問題,而且很難確認發生這種問題的原因。仔細考慮問題是如何通過系統傳播的,以及如何最終呈現給用戶,你會為自己,團隊和用戶減少很多痛苦。 在“錯誤處理”一節中,我們討論了如何從goroutine處理錯誤,但我們沒有花時間討論這些錯誤應該是什么樣子,或者錯誤應該如何流經一個龐大而復雜的系統。讓我們花點時間來討論錯誤傳遞的哲學。
許多開發人員認為錯誤傳遞是不值得關注的,或者,至少不是首先需要關注的。 Go試圖通過強制開發者在調用堆棧中的每一幀處理錯誤來糾正這種不良做法。
首先讓我們看看錯誤的定義。錯誤何時發生,以及錯誤會提供什么。
錯誤表明您的系統已進入無法完成用戶明確或隱含請求的操作的狀態。 因此,它需要傳遞一些關鍵信息:
***發生了什么***
這是錯誤的一部分,其中包含有關所發生事件的信息,例如“磁盤已滿”,“套接字已關閉”或“憑證過期”。盡管生成錯誤的內容可能會隱式生成此信息,你可以用一些能夠幫助用戶的上下文來完善它。
***何時何處發生***
錯誤應始終包含一個完整的堆棧跟蹤,從調用的啟動方式開始,直到實例化錯誤。
此外,錯誤應該包含有關它正在運行的上下文的信息。 例如,在分布式系統中,它應該有一些方法來識別發生錯誤的機器。當試圖了解系統中發生的情況時,這些信息將具有無法估量的價值。
另外,錯誤應該包含錯誤實例化的機器上的時間,以UTC表示。
***有效的信息說明***
顯示給用戶的消息應該進行自定義以適合你的系統及其用戶。它只應包含前兩點的簡短和相關信息。 一個友好的信息是以人為中心的,給出一些關于這個問題的指示,并且應該是關于一行文本。
***如何獲取更詳細的錯誤信息***
在某個時刻,有人可能想詳細了解發生錯誤時的系統狀態。提供給用戶的錯誤信息應該包含一個ID,該ID可以與相應的日志交叉引用,該日志顯示錯誤的完整信息:發生錯誤的時間(不是錯誤記錄的時間),堆棧跟蹤——包括你在代碼中自定義的信息。包含堆棧跟蹤的哈希也是有幫助的,以幫助在bug跟蹤器中匯總類似的問題。
默認情況下,沒有人工干預,錯誤所能提供的信息少得可憐。 因此,我們可以認為:在沒有詳細信息的情況下傳播給用戶任何錯誤的行為都是錯誤的。因為我們可以使用搭建框架的思路來對待錯誤處理。可以將所有錯誤歸納為兩個類別:
* Bug。
* 已知業務及系統意外(例如,網絡連接斷開,磁盤寫入失敗等)。
Bug是你沒有為系統定制的錯誤,或者是“原始”錯誤。有時這是故意的,如果在系統多次迭代時出現的錯誤,盡快不可避免的傳遞給了用戶,但接受到用戶反饋后對提高系統健壯性并不是壞處。有時這是偶然的。在確定如何傳播錯誤,系統隨著時間的推移如何增長以及最終向用戶展示什么時,這種區別將證明是有用的。
想象下一個巨大的系統,包含了很多模塊:
:-: 
假設在“Low Level Component”中發生錯誤,并且我們已經制作了一個格式良好的錯誤,并傳遞給堆棧。 在“Low Level Component”的背景下,這個錯誤可能被認為是合理的,但在我們的系統中,它可能不是。 讓我們看看在每個組件的邊界處,所有傳入的錯誤都必須包含在我們代碼所在組件的格式錯誤中。 例如,如果我們處于“Intermediary Component”,并且我們從“Low Level Component”調用代碼,這可能會出錯,我們可以使用:
```
func PostReport(id string) error {
result, err := lowlevel.DoWork()
if err != nil {
if _, ok := err.(lowlevel.Error); ok { //1
err = WrapErr(err, "cannot post report with id %q", id) //2
}
// ...
}
}
```
1. 我們在這里斷言以確定是我們自定義的錯誤。如果不是,我們會簡單的把err傳遞給堆棧表明這里發生的錯誤是個bug。
2. 在這里,我們使用函數將傳入的錯誤與我們模塊的相關信息進行封裝,并給它一個新類型。請注意,包裝錯誤可能隱藏一些底層細節。
在錯誤最初被實例化時,錯誤發生時的底層細節是存在于錯誤信息中的。在我們的示例中,模塊的邊界處我們將錯誤包裝起來,不屬于我們定義的錯誤類型的錯誤都被視為格式錯誤。請注意,實際工作中建議只以你自己的模塊邊界(公共函數/方法)或代碼添加有價值的上下文時以這種方式包裝錯誤。
采取這種立場可以讓我們的系統有機地發展。 我們可以確定傳入的錯誤是正確的,反過來可以確保考慮錯誤如何離開模塊。錯誤正確性成為我們系統的一個新特性。通過這樣做,我們給出了一個思想上的框架,通過呈現給用戶的內容明確劃分了錯誤的類型。
所有的錯誤都應該記錄下盡可能多的信息。 但是,當向用戶顯示錯誤時,就需要盡可能的清晰明了。
當我們的代碼發現到一個格式良好的錯誤時,我們可以確信,在代碼中的所有級別上,都意識到了該錯誤的存在,而且已經將其記錄下來并打印出來供用戶查看。
當錯誤傳播給用戶時,我們記錄錯誤,同時向用戶顯示一條友好的消息,指出發生了意外事件。如果我們的系統中支持自動錯誤報告,那是最好不過的事情。如果不支持,應當建議用戶提交一個錯誤報告。請注意,任何微小的錯誤都會包含有用的信息,即使我們無法保證面面俱到。
請記住,在任何一種情況下,如果出現錯誤或格式錯誤,我們將在郵件中包含一個日志ID,以便在需要更多信息時可以參考。
我們來看一個完整的例子。 這個例子不會非常健壯(例如,錯誤類型可能是簡單化的),并且調用堆棧是線性的,但不妨礙大家來理清思路:
```
type MyError struct {
Inner error
Message string
StackTrace string
Misc map[string]interface{}
}
func wrapError(err error, messagef string, msgArgs ...interface{}) MyError {
return MyError{
Inner: err, //1
Message: fmt.Sprintf(messagef, msgArgs...),
StackTrace: string(debug.Stack()), //2
Misc: make(map[string]interface{}), //3
}
}
func (err MyError) Error() string {
return err.Message
}
```
1. 在這里存儲我們正在包裝的錯誤。 如果需要調查發生的事情,我們總是希望能夠查看到最低級別的錯誤。
2. 這行代碼記錄了創建錯誤時的堆棧跟蹤。
3. 這里我們創建一個雜項信息存儲字段。可以存儲并發ID,堆棧跟蹤的hash或可能有助于診斷錯誤的其他上下文信息。
接下來,我們建立一個名為 lowlevel 的模塊:
```
// "lowlevel" module
type LowLevelErr struct {
error
}
func isGloballyExec(path string) (bool, error) {
info, err := os.Stat(path)
if err != nil {
return false, LowLevelErr{wrapError(err, err.Error())} // 1
}
return info.Mode().Perm()&0100 == 0100, nil
}
```
1. 在這里,我們用自定義錯誤來封裝os.Stat中的原始錯誤。在這種情況下,我們不會掩蓋這個錯誤產生的信息。
然后我們建立另一個名為 intermediate 的模塊,它會調用 lowlevel 所在的包:
```
// "intermediate" module
type IntermediateErr struct {
error
}
func runJob(id string) error {
const jobBinPath = "/bad/job/binary"
isExecutable, err := isGloballyExec(jobBinPath)
if err != nil {
return err //1
} else if isExecutable == false {
return wrapError(nil, "job binary is not executable")
}
return exec.Command(jobBinPath, "--id="+id).Run() //1
}
```
1. 我們傳遞來自 lowlevel 模塊的錯誤,由于我們接收從其他模塊傳遞的錯誤而沒有將它們包裝在我們自己的錯誤類型中,這將會產生問題。
最后,讓我們創建一個調用intermediate包函數的頂級main函數:
```
func handleError(key int, err error, message string) {
log.SetPrefix(fmt.Sprintf("[logID: %v]: ", key))
log.Printf("%#v", err) //3
fmt.Printf("[%v] %v", key, message)
}
func main() {
log.SetOutput(os.Stdout)
log.SetFlags(log.Ltime | log.LUTC)
err := runJob("1")
if err != nil {
msg := "There was an unexpected issue; please report this as a bug."
if _, ok := err.(IntermediateErr); ok { //1
msg = err.Error()
}
handleError(1, err, msg) //2
}
}
```
1. 在這里我們檢查是否錯誤是預期的類型。 如果是,可以簡單地將其消息傳遞給用戶。
2. 在這一行中,將日志和錯誤消息與ID綁定在一起。我們可以很容易增加這個增量,或者使用一個GUID來確保一個唯一的ID。
3. 在這里我們記錄完整的錯誤,以備需要深入了解發生了什么。
我們在運行后會在日志中發現:
```
[logID: 1]: 21:46:07 main.LowLevelErr{error:main.MyError{Inner: (*os.PathError)(0xc4200123f0),
Message:"stat /bad/job/binary: no such file or directory", StackTrace:"goroutine 1 [running]: runtime/debug.Stack(0xc420012420, 0x2f, 0xc420045d80)
/home/kate/.guix-profile/src/runtime/debug/stack.go:24 +0x79 main.wrapError(0x530200, 0xc4200123f0, 0xc420012420, 0x2f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, ...)
/tmp/babel-79540aE/go-src-7954NTK.go:22 +0x62 main.isGloballyExec(0x4d1313, 0xf, 0xc420045eb8, 0x487649, 0xc420056050)
/tmp/babel-79540aE/go-src-7954NTK.go:37 +0xaa main.runJob(0x4cfada, 0x1, 0x4d4c35, 0x22)
/tmp/babel-79540aE/go-src-7954NTK.go:47 +0x48 main.main()
/tmp/babel-79540aE/go-src-7954NTK.go:67 +0x63 ", Misc:map[string]interface {}{}}}
```
并且標準輸出會打印:
```
[1]There was an unexpected issue; please report this as a bug.
```
我們可以看到,在這個錯誤路徑的某處,它沒有正確處理,并且因為我們無法確定錯誤信息是否適合用戶自行處理,所以我們輸出一個簡單的錯誤信息,指出意外事件發生了。如果回顧 lowlevel 模塊,我們會發現錯誤發生的原因:我們沒有包裝來自 lowlevel 模塊的錯誤。讓我們糾正它:
```
// "intermediate" module
type IntermediateErr struct {
error
}
func runJob(id string) error {
const jobBinPath = "/bad/job/binary"
isExecutable, err := isGloballyExec(jobBinPath)
if err != nil {
return IntermediateErr{wrapError(err,
"cannot run job %q: requisite binaries not available", id)} //1
} else if isExecutable == false {
return wrapError(
nil,
"cannot run job %q: requisite binaries are not executable", id,
)
}
return exec.Command(jobBinPath, "--id="+id).Run()
}
```
1. 在這里,我們現在使用自定義錯誤。我們想隱藏工作未運行原因的底層細節,因為這對于用戶并不重要。
```
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 := runJob("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)
}
}
```
現在,當我們運行更新后的代碼,會得到類似的日志:
```
[logID: 1]: 22:11:04 main.IntermediateErr{error:main.MyError
{Inner:main.LowLevelErr{error:main.MyError{Inner:(*os.PathError) (0xc4200123f0), Message:"stat /bad/job/binary: no such file or directory", StackTrace:"goroutine 1 [running]:
runtime/debug.Stack(0xc420012420, 0x2f, 0x0)
/home/kate/.guix-profile/src/runtime/debug/stack.go:24 +0x79 main.wrapError(0x530200, 0xc4200123f0, 0xc420012420, 0x2f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, ...)
/tmp/babel-79540aE/go -src-7954DTN.go:22 +0xbb main.isGloballyExec(0x4d1313, 0xf, 0x4daecc, 0x30, 0x4c5800)
/tmp/babel-79540aE/go -src-7954DTN.go:39 +0xc5 main.runJob(0x4cfada, 0x1, 0x4d4c19, 0x22)
/tmp/babel-79540aE/go-src-7954DTN.go:51 +0x4b
main.main()
/tmp/babel-79540aE/go -src-7954DTN.go:71 +0x63
", Misc:map[string]interface {}{}}}, Message:"cannot run job \"1\": requisite binaries not available", StackTrace:"goroutine 1 [running]: runtime/debug.Stack(0x4d63f0, 0x33, 0xc420045e40)
/home/kate/.guix-profile/src/runtime/debug/stack.go:24 +0x79 main.wrapError(0x530380, 0xc42000a370, 0x4d63f0, 0x33, 0xc420045e40, 0x1, 0x1, 0x0, 0x0, 0x0, ...)
/tmp/babel-79540aE/go -src-7954DTN.go:22 +0xbb main.runJob(0x4cfada, 0x1, 0x4d4c19, 0x22)
/tmp/babel-79540aE/go -src-7954DTN.go:53 +0x356 main.main()
/tmp/babel-79540aE/go -src-7954DTN.go:71 +0x63 ", Misc:map[string]interface {}{}}}
```
錯誤信息變得十分明白:
```
[1]cannot run job "1": requisite binaries not available
```
這種實現方法與標準庫的錯誤包兼容,此外你可以用你喜歡的任何方式來進行包裝,并且自由度非常大。
* * * * *
學識淺薄,錯誤在所難免。我是長風,歡迎來Golang中國的群(211938256)就本書提出修改意見。
- 前序
- 誰適合讀這本書
- 章節導讀
- 在線資源
- 第一章 并發編程介紹
- 摩爾定律,可伸縮網絡和我們所處的困境
- 為什么并發編程如此困難
- 數據競爭
- 原子性
- 內存訪問同步
- 死鎖,活鎖和鎖的饑餓問題
- 死鎖
- 活鎖
- 饑餓
- 并發安全性
- 優雅的面對復雜性
- 第二章 代碼建模:序列化交互處理
- 并發與并行
- 什么是CSP
- CSP在Go中的衍生物
- Go的并發哲學
- 第三章 Go的并發構建模塊
- Goroutines
- sync包
- WaitGroup
- Mutex和RWMutex
- Cond
- Once
- Pool
- Channels
- select語句
- GOMAXPROCS
- 結論
- 第四章 Go的并發編程范式
- 訪問范圍約束
- fo-select循環
- 防止Goroutine泄漏
- or-channel
- 錯誤處理
- 管道
- 構建管道的最佳實踐
- 便利的生成器
- 扇入扇出
- or-done-channel
- tee-channel
- bridge-channel
- 隊列
- context包
- 小結
- 第五章 可伸縮并發設計
- 錯誤傳遞
- 超時和取消
- 心跳
- 請求并發復制處理
- 速率限制
- Goroutines異常行為修復
- 本章小結
- 第六章 Goroutines和Go運行時
- 任務調度