關(guān)于Go程序錯(cuò)誤處理的一些建議
Go 的錯(cuò)誤處理這塊是日常被大家吐槽較多的地方,我在工作中也觀察到一些現(xiàn)象,比較嚴(yán)重的是在各層級(jí)的邏輯代碼中對(duì)錯(cuò)誤的處理有些重復(fù)。
比如,有人寫(xiě)代碼就會(huì)在每一層都判斷錯(cuò)誤并記錄日志,從代碼層面看,貌似很嚴(yán)謹(jǐn),但是如果看日志會(huì)發(fā)現(xiàn)一堆重復(fù)的信息,等到排查問(wèn)題時(shí)反而會(huì)造成干擾。
今天給大家總結(jié)三點(diǎn) Go 代碼錯(cuò)誤處理相關(guān)的最佳實(shí)踐給大家。
這些最佳實(shí)踐也是網(wǎng)上一些前輩分享的,我自己實(shí)踐后在這里用自己的語(yǔ)言描述出來(lái),希望能對(duì)大家有所幫助。
認(rèn)識(shí)error
Go 程序通過(guò) error 類型的值表示錯(cuò)誤
error 類型是一個(gè)內(nèi)建接口類型,該接口只規(guī)定了一個(gè)返回字符串值的 Error 方法。
- type error interface {
 - Error() string
 - }
 
Go 語(yǔ)言的函數(shù)經(jīng)常會(huì)返回一個(gè) error 值,調(diào)用者通過(guò)測(cè)試 error 值是否是 nil 來(lái)進(jìn)行錯(cuò)誤處理。
- i, err := strconv.Atoi("42")
 - if err != nil {
 - fmt.Printf("couldn't convert number: %v\n", err)
 - return
 - }
 - fmt.Println("Converted integer:", i)
 
error 為 nil 時(shí)表示成功;非 nil 的 error 表示失敗。
自定義錯(cuò)誤記得要實(shí)現(xiàn)error接口
我們經(jīng)常會(huì)定義符合自己需要的錯(cuò)誤類型,但是記住要讓這些類型實(shí)現(xiàn) error 接口,這樣就不用在調(diào)用方的程序里引入額外的類型。
比如下面我們自己定義了 myError 這個(gè)類型,如果不實(shí)現(xiàn) error 接口的話,調(diào)用者的代碼中就會(huì)被 myError 這個(gè)類型侵入。比如下面的 run 函數(shù),在定義返回值類型時(shí),直接定義成 error 即可。
- package myerror
 - import (
 - "fmt"
 - "time"
 - )
 - type myError struct {
 - Code int
 - When time.Time
 - What string
 - }
 - func (e *myError) Error() string {
 - return fmt.Sprintf("at %v, %s, code %d",
 - e.When, e.What, e.Code)
 - }
 - func run() error {
 - return &MyError{
 - 1002,
 - time.Now(),
 - "it didn't work",
 - }
 - }
 - func TryIt() {
 - if err := run(); err != nil {
 - fmt.Println(err)
 - }
 - }
 
如果 myError 不實(shí)現(xiàn) error 接口的話,這里的返回 值類型就要 定義成 myError 類型。 可想而知,緊接著調(diào)用者的程序里就要通過(guò) myError.Code == xxx  來(lái)判斷到底是哪種具體的錯(cuò)誤(當(dāng)然想要這么干得先把 myError 改成導(dǎo)出的 MyError )。
那調(diào)用者判斷自定義 error 是具體哪種錯(cuò)誤的時(shí)候應(yīng)該怎么辦呢, myError 并未向包外暴露,答案是通過(guò)向包外暴露檢查錯(cuò)誤行為的方法來(lái)實(shí)現(xiàn)。
- myerror.IsXXXError(err)
 - ...
 
抑或是通過(guò)比較 error 本身與包向外暴露的常量錯(cuò)誤是否相等來(lái)判斷,比如操作文件時(shí)常用來(lái)判斷文件是否結(jié)束的 io.EOF 。
類似的還有 gorm.ErrRecordNotFound 等各種開(kāi)源包對(duì)外暴露的錯(cuò)誤常量。
- if err != io.EOF {
 - return err
 - }
 
錯(cuò)誤處理常犯的錯(cuò)誤
先看一段簡(jiǎn)單的程序,看大家能不能發(fā)現(xiàn)一些細(xì)微的問(wèn)題
- func WriteAll(w io.Writer, buf []byte) error {
 - _, err := w.Write(buf)
 - if err != nil {
 - log.Println("unable to write:", err) // annotated error goes to log file
 - return err // unannotated error returned to caller
 - }
 - return nil
 - }
 - func WriteConfig(w io.Writer, conf *Config) error {
 - buf, err := json.Marshal(conf)
 - if err != nil {
 - log.Printf("could not marshal config: %v", err)
 - return err
 - }
 - if err := WriteAll(w, buf); err != nil {
 - log.Println("could not write config: %v", err)
 - return err
 - }
 - return nil
 - }
 - func main() {
 - err := WriteConfig(f, &conf)
 - fmt.Println(err) // io.EOF
 - }
 
錯(cuò)誤處理常犯的兩個(gè)問(wèn)題
上面程序的錯(cuò)誤處理暴露了兩個(gè)問(wèn)題:
1. 底層函數(shù) WriteAll 在發(fā)生錯(cuò)誤后,除了向上層返回錯(cuò)誤外還向日志里記錄了錯(cuò)誤,上層調(diào)用者做了同樣的事情,記錄日志然后把錯(cuò)誤再返回給程序頂層。
因此在日志文件中得到一堆重復(fù)的內(nèi)容
- unable to write: io.EOF
 - could not write config: io.EOF
 - ...
 
2. 在程序的頂部,雖然得到了原始錯(cuò)誤,但沒(méi)有相關(guān)內(nèi)容,換句話說(shuō)沒(méi)有把 WriteAll 、 WriteConfig 記錄到 log 里的那些信息包裝到錯(cuò)誤里,返回給上層。
針對(duì)這兩個(gè)問(wèn)題的解決方案可以是,在底層函數(shù) WriteAll 、 WriteConfig 中為發(fā)生的錯(cuò)誤添加上下文信息,然后將錯(cuò)誤返回上層,由上層程序最后處理這些錯(cuò)誤。
一種簡(jiǎn)單的包裝錯(cuò)誤的方法是使用 fmt.Errorf 函數(shù),給錯(cuò)誤添加信息。
- func WriteConfig(w io.Writer, conf *Config) error {
 - buf, err := json.Marshal(conf)
 - if err != nil {
 - return fmt.Errorf("could not marshal config: %v", err)
 - }
 - if err := WriteAll(w, buf); err != nil {
 - return fmt.Errorf("could not write config: %v", err)
 - }
 - return nil
 - }
 - func WriteAll(w io.Writer, buf []byte) error {
 - _, err := w.Write(buf)
 - if err != nil {
 - return fmt.Errorf("write failed: %v", err)
 - }
 - return nil
 - }
 
給錯(cuò)誤附加上下文信息
fmt.Errorf 只是給錯(cuò)誤添加了簡(jiǎn)單的注解信息,如果你想在添加信息的同時(shí)還加上錯(cuò)誤的調(diào)用棧,可以借助 github.com/pkg/errors 這個(gè)包提供的錯(cuò)誤包裝能力。
- //只附加新的信息
 - func WithMessage(err error, message string) error
 - //只附加調(diào)用堆棧信息
 - func WithStack(err error) error
 - //同時(shí)附加堆棧和信息
 - func Wrap(err error, message string) error
 
有包裝方法,就有對(duì)應(yīng)的解包方法, Cause 方法會(huì)返回包裝錯(cuò)誤對(duì)應(yīng)的最原始錯(cuò)誤--即會(huì)遞歸地進(jìn)行解包。
- func Cause(err error) error
 
下面是使用 github.com/pkg/errors 改寫(xiě)后的錯(cuò)誤處理程序
- func ReadFile(path string) ([]byte, error) {
 - f, err := os.Open(path)
 - if err != nil {
 - return nil, errors.Wrap(err, "open failed")
 - }
 - defer f.Close()
 - buf, err := ioutil.ReadAll(f)
 - if err != nil {
 - return nil, errors.Wrap(err, "read failed")
 - }
 - return buf, nil
 - }
 - func ReadConfig() ([]byte, error) {
 - home := os.Getenv("HOME")
 - config, err := ReadFile(filepath.Join(home, ".settings.xml"))
 - return config, errors.WithMessage(err, "could not read config")
 - }
 - func main() {
 - _, err := ReadConfig()
 - if err != nil {
 - fmt.Printf("original error: %T %v\n", errors.Cause(err), errors.Cause(err))
 - fmt.Printf("stack trace:\n%+v\n", err)
 - os.Exit(1)
 - }
 - }
 
上面格式化字符串時(shí)用的 %+v 是在 % v 基礎(chǔ)上,對(duì)值進(jìn)行展開(kāi),即展開(kāi)復(fù)合類型值,比如結(jié)構(gòu)體的字段值等明細(xì)。
這樣既能給錯(cuò)誤添加調(diào)用棧信息,又能保留對(duì)原始錯(cuò)誤的引用,通過(guò) Cause 可以還原到最初始引發(fā)錯(cuò)誤的原因。
總結(jié)
總結(jié)一下,錯(cuò)誤處理的原則就是:
- 
    
錯(cuò)誤只在邏輯的最外層處理一次,底層只返回錯(cuò)誤。
 - 
    
底層除了返回錯(cuò)誤外,要對(duì)原始錯(cuò)誤進(jìn)行包裝,增加錯(cuò)誤信息、調(diào)用棧等這些有利于排查的上下文信息。
 















 
 
 








 
 
 
 