一篇文章帶你輕松搞懂Golang的error處理
Golang中的error
Golang中的 error 就是一個(gè)簡(jiǎn)單的接口類型。只要實(shí)現(xiàn)了這個(gè)接口,就可以將其視為一種 error
type error interface { Error() string }
error的幾種玩法
翻看Golang源碼,能看到許多類似于下面的這兩種error類型
哨兵錯(cuò)誤
var EOF = errors.New("EOF") var ErrUnexpectedEOF = errors.New("unexpected EOF") var ErrNoProgress = errors.New("multiple Read calls return no data or error")
缺點(diǎn):
1.讓 error 具有二義性
error != nil不再意味著一定發(fā)生了錯(cuò)誤
比如io.Reader返回io.EOF來(lái)告知調(diào)用者沒(méi)有更多數(shù)據(jù)了,然而這又不是一個(gè)錯(cuò)誤
2.在兩個(gè)包之間創(chuàng)建了依賴
如果你使用了io.EOF來(lái)檢查是否read完所有的數(shù)據(jù),那么代碼里一定會(huì)導(dǎo)入io包
自定義錯(cuò)誤類型
一個(gè)不錯(cuò)的例子是os.PathError,它的優(yōu)點(diǎn)是可以附帶更多的上下文信息
type PathError struct { Op string Path string Err error }
Wrap error
到這里我們可以發(fā)現(xiàn),Golang 的 error 非常簡(jiǎn)單,然而簡(jiǎn)單也意味著有時(shí)候是不夠用的
Golang的error一直有兩個(gè)問(wèn)題:
1.error沒(méi)有附帶file:line信息(也就是沒(méi)有堆棧信息)
比如這種error,鬼知道代碼哪一行報(bào)了錯(cuò),Debug時(shí)簡(jiǎn)直要命
SERVICE ERROR 2022-03-25T16:32:10.687+0800!!!
Error 1406: Data too long for column 'content' at row 1
2.上層error想附帶更多日志信息時(shí),往往會(huì)使用fmt.Errorf()
,fmt.Errorf()
會(huì)創(chuàng)建一個(gè)新的error,底層的error類型就被“吞”掉了
var errNoRows = errors.New("no rows") // 模仿sql庫(kù)返回一個(gè)errNoRows func sqlExec() error { return errNoRows } func serviceNoErrWrap() error { err := sqlExec() if err != nil { return fmt.Errorf("sqlExec failed.Err:%v", err) } return nil } func TestErrWrap(t *testing.T) { // 使用fmt.Errorf創(chuàng)建了一個(gè)新的err,丟失了底層err err := serviceNoErrWrap() if err != errNoRows { log.Println("===== errType don't equal errNoRows =====") } } -------------------------------代碼運(yùn)行結(jié)果---------------------------------- === RUN TestErrWrap 2022/03/26 17:19:43 ===== errType don't equal errNoRows =====
為了解決這個(gè)問(wèn)題,我們可以使用github.com/pkg/error包
,使用errors.withStack()方法
將err保
存到withStack對(duì)象
// withStack結(jié)構(gòu)體保存了error,形成了一條error鏈。同時(shí)*stack字段保存了堆棧信息。 type withStack struct { error *stack }
也可以使用errors.Wrap(err, "自定義文本")
,額外附帶一些自定義的文本信息
源碼解讀:先將err和message包進(jìn)withMessage對(duì)象
,再將withMessage對(duì)象
和堆棧信息包進(jìn)withStack對(duì)象
func Wrap(err error, message string) error { if err == nil { return nil } err = &withMessage{ cause: err, msg: message, } return &withStack{ err, callers(), } }
Golang1.13版本error的新特性
Golang1.13版本借鑒了github.com/pkg/error包
,新增了如下函數(shù),大大增強(qiáng)了 Golang 語(yǔ)言判斷 error 類型的能力
errors.UnWrap()
// 與errors.Wrap()行為相反 // 獲取err鏈中的底層err func Unwrap(err error) error { u, ok := err.(interface { Unwrap() error }) if !ok { return nil } return u.Unwrap() }
errors.Is()
在1.13版本之前,我們可以用err == targetErr
判斷err類型errors.Is()
是其增強(qiáng)版:error 鏈上的任一err == targetErr
,即return true
// 實(shí)踐:學(xué)習(xí)使用errors.Is() var errNoRows = errors.New("no rows") // 模仿sql庫(kù)返回一個(gè)errNoRows func sqlExec() error { return errNoRows } func service() error { err := sqlExec() if err != nil { return errors.WithStack(err) // 包裝errNoRows } return nil } func TestErrIs(t *testing.T) { err := service() // errors.Is遞歸調(diào)用errors.UnWrap,命中err鏈上的任意err即返回true if errors.Is(err, errNoRows) { log.Println("===== errors.Is() succeeded =====") } //err經(jīng)errors.WithStack包裝,不能通過(guò) == 判斷err類型 if err == errNoRows { log.Println("err == errNoRows") } } -------------------------------代碼運(yùn)行結(jié)果---------------------------------- === RUN TestErrIs 2022/03/25 18:35:00 ===== errors.Is() succeeded =====
例子解讀:
因?yàn)槭褂?code>errors.WithStack包裝了sqlError
,sqlError
位于error鏈的底層,上層的error已經(jīng)不再是sqlError
類型,所以使用==
無(wú)法判斷出底層的sqlError
源碼解讀:
- 我們很容易想到其內(nèi)部調(diào)用了
err = Unwrap(err)
方法來(lái)獲取error鏈中底層的error - 自定義error類型可以實(shí)現(xiàn)
Is接口
來(lái)自定義error類型判斷方法
func Is(err, target error) bool { if target == nil { return err == target } isComparable := reflectlite.TypeOf(target).Comparable() for { if isComparable && err == target { return true } // 支持自定義error類型判斷 if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) { return true } if err = Unwrap(err); err == nil { return false } } }
下面我們來(lái)看看如何自定義error類型判斷:
自定義的errNoRows類型
,必須實(shí)現(xiàn)Is接口
,才能使用erros.Is()
進(jìn)行類型判斷
type errNoRows struct { Desc string } func (e errNoRows) Unwrap() error { return e } func (e errNoRows) Error() string { return e.Desc } func (e errNoRows) Is(err error) bool { return reflect.TypeOf(err).Name() == reflect.TypeOf(e).Name() } // 模仿sql庫(kù)返回一個(gè)errNoRows func sqlExec() error { return &errNoRows{"Kaolengmian NB"} } func service() error { err := sqlExec() if err != nil { return errors.WithStack(err) } return nil } func serviceNoErrWrap() error { err := sqlExec() if err != nil { return fmt.Errorf("sqlExec failed.Err:%v", err) } return nil } func TestErrIs(t *testing.T) { err := service() if errors.Is(err, errNoRows{}) { log.Println("===== errors.Is() succeeded =====") } } -------------------------------代碼運(yùn)行結(jié)果---------------------------------- === RUN TestErrIs 2022/03/25 18:35:00 ===== errors.Is() succeeded =====
errors.As()
在1.13版本之前,我們可以用if _,ok := err.(targetErr)
判斷err類型errors.As()
是其增強(qiáng)版:error 鏈上的任一err與targetErr類型相同
,即return true
// 通過(guò)例子學(xué)習(xí)使用errors.As() type sqlError struct { error } func (e *sqlError) IsNoRows() bool { t, ok := e.error.(ErrNoRows) return ok && t.IsNoRows() } type ErrNoRows interface { IsNoRows() bool } // 返回一個(gè)sqlError func sqlExec() error { return sqlError{} } // errors.WithStack包裝sqlError func service() error { err := sqlExec() if err != nil { return errors.WithStack(err) } return nil } func TestErrAs(t *testing.T) { err := service() // 遞歸使用errors.UnWrap,只要Err鏈上有一種Err滿足類型斷言,即返回true sr := &sqlError{} if errors.As(err, sr) { log.Println("===== errors.As() succeeded =====") } // 經(jīng)errors.WithStack包裝后,不能通過(guò)類型斷言將當(dāng)前Err轉(zhuǎn)換成底層Err if _, ok := err.(sqlError); ok { log.Println("===== type assert succeeded =====") } } ----------------------------------代碼運(yùn)行結(jié)果-------------------------------------------- === RUN TestErrAs 2022/03/25 18:09:02 ===== errors.As() succeeded =====
例子解讀:
因?yàn)槭褂?code>errors.WithStack包裝了sqlError
,sqlError
位于error鏈的底層,上層的error已經(jīng)不再是sqlError
類型,所以使用類型斷言無(wú)法判斷出底層的sqlError
error處理最佳實(shí)踐
上面講了如何定義error類型,如何比較error類型,現(xiàn)在我們談?wù)勅绾卧诖笮晚?xiàng)目中做好error處理
優(yōu)先處理error
當(dāng)一個(gè)函數(shù)返回一個(gè)非空error時(shí),應(yīng)該優(yōu)先處理error,忽略它的其他返回值
只處理error一次
- 在Golang中,對(duì)于每個(gè)err,我們應(yīng)該只處理一次。
- 要么立即處理err(包括記日志等行為),return nil(把錯(cuò)誤吞掉)。此時(shí)因?yàn)榘彦e(cuò)誤做了降級(jí),一定要小心處理函數(shù)返回值。
比如下面例子json.Marshal(conf)沒(méi)有return err ,那么在使用buf時(shí)一定要小心空指針等錯(cuò)誤
要么return err,在上層處理err
反例:
// 試想如果writeAll函數(shù)出錯(cuò),會(huì)打印兩遍日志 // 如果整個(gè)項(xiàng)目都這么做,最后會(huì)驚奇的發(fā)現(xiàn)我們?cè)谔幪幋蛉罩?,?xiàng)目中存在大量沒(méi)有價(jià)值的垃圾日志 // unable to write:io.EOF // could not write config:io.EOF type config struct {} func writeAll(w io.Writer, buf []byte) error { _, err := w.Write(buf) if err != nil { log.Println("unable to write:", err) return err } 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) } if err := writeAll(w, buf); err != nil { log.Println("count not write config: %v", err) return err } return nil }
不要反復(fù)包裝error
我們應(yīng)該包裝error,但只包裝一次
上層業(yè)務(wù)代碼建議Wrap error
,但是底層基礎(chǔ)Kit庫(kù)不建議
如果底層基礎(chǔ) Kit 庫(kù)包裝了一次,上層業(yè)務(wù)代碼又包裝了一次,就重復(fù)包裝了 error,日志就會(huì)打重
比如我們常用的sql庫(kù)
會(huì)返回sql.ErrNoRows
這種預(yù)定義錯(cuò)誤,而不是給我們一個(gè)包裝過(guò)的 error
不透明的錯(cuò)誤處理
在大型項(xiàng)目中,推薦使用不透明的錯(cuò)誤處理(Opaque errors)
:不關(guān)心錯(cuò)誤類型,只關(guān)心error是否為nil
好處:
耦合小,不需要判斷特定錯(cuò)誤類型,就不需要導(dǎo)入相關(guān)包的依賴。
不過(guò)有時(shí)候,這種處理error的方式不夠用,比如:業(yè)務(wù)需要對(duì)參數(shù)異常error類型
做降級(jí)處理,打印Warn級(jí)別的日志
type ParamInvalidError struct { Desc string } func (e ParamInvalidError) Unwrap() error { return e } func (e ParamInvalidError) Error() string { return "ParamInvalidError: " + e.Desc } func (e ParamInvalidError) Is(err error) bool { return reflect.TypeOf(err).Name() == reflect.TypeOf(e).Name() } func NewParamInvalidErr(desc string) error { return errors.WithStack(&ParamInvalidError{Desc: desc}) } ------------------------------頂層打印日志--------------------------------- if errors.Is(err, Err.ParamInvalidError{}) { logger.Warnf(ctx, "%s", err.Error()) return } if err != nil { logger.Errorf(ctx, " error:%+v", err) }
簡(jiǎn)化錯(cuò)誤處理
Golang因?yàn)榇a中無(wú)數(shù)的if err != nil
被詬病,現(xiàn)在我們看看如何減少if err != nil
這種代碼
bufio.scan
CountLines() 實(shí)現(xiàn)了"讀取內(nèi)容的行數(shù)"功能
可以利用 bufio.scan() 簡(jiǎn)化 error 的處理:
func CountLines(r io.Reader) (int, error) { var ( br = bufio.NewReader(r) lines int err error ) for { _, err := br.ReadString('\n') lines++ if err != nil { break } } if err != io.EOF { return 0, nilsadwawa } return lines, nil } func CountLinesGracefulErr(r io.Reader) (int, error) { sc := bufio.NewScanner(r) lines := 0 for sc.Scan() { lines++ } return lines, sc.Err() }
bufio.NewScanner()
返回一個(gè) Scanner
對(duì)象,結(jié)構(gòu)體內(nèi)部包含了 error 類型,調(diào)用Err()
方法即可返回封裝好的error
Golang源代碼中蘊(yùn)含著大量的優(yōu)秀設(shè)計(jì)思想,我們?cè)陂喿x源碼時(shí)從中學(xué)習(xí),并在實(shí)踐中得以運(yùn)用
type Scanner struct { r io.Reader // The reader provided by the client. split SplitFunc // The function to split the tokens. maxTokenSize int // Maximum size of a token; modified by tests. token []byte // Last token returned by split. buf []byte // Buffer used as argument to split. start int // First non-processed byte in buf. end int // End of data in buf. err error // Sticky error. empties int // Count of successive empty tokens. scanCalled bool // Scan has been called; buffer is in use. done bool // Scan has finished. } func (s *Scanner) Err() error { if s.err == io.EOF { return nil } return s.err }
errWriter
WriteResponse()
函數(shù)實(shí)現(xiàn)了"構(gòu)建HttpResponse"
功能
利用上面學(xué)到的思路,我們可以自己實(shí)現(xiàn)一個(gè)errWriter
對(duì)象,簡(jiǎn)化對(duì) error 的處理
type Header struct { Key, Value string } type Status struct { Code int Reason string } func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error { _, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason) if err != nil { return err } for _, h := range headers { _, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value) if err != nil { return err } } if _, err := fmt.Fprintf(w, "\r\n"); err != nil { return err } _, err = io.Copy(w, body) return err } type errWriter struct { io.Writer err error } func (e *errWriter) Write(buf []byte) (n int, err error) { if e.err != nil { return 0, e.err } n, e.err = e.Writer.Write(buf) return n, nil } func WriteResponseGracefulErr(w io.Writer, st Status, headers []Header, body io.Reader) error { ew := &errWriter{w, nil} fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason) for _, h := range headers { fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value) } fmt.Fprintf(w, "\r\n") io.Copy(ew, body) return ew.err }
何時(shí)該用panic
在 Golang 中panic
會(huì)導(dǎo)致程序直接退出,是一個(gè)致命的錯(cuò)誤。
建議發(fā)生致命的程序錯(cuò)誤時(shí)才使用 panic,例如索引越界、不可恢復(fù)的環(huán)境問(wèn)題、棧溢出等等
小補(bǔ)充
errors.New()
返回的是errorString對(duì)象
的指針,其原因是防止字符串產(chǎn)生碰撞,如果發(fā)生碰撞,兩個(gè) error 對(duì)象會(huì)相等。
源碼:
func New(text string) error { return &errorString{text} } // errorString is a trivial implementation of error. type errorString struct { s string } func (e *errorString) Error() string { return e.s }
實(shí)踐:error1
和error2
的text都是"error"
,但是二者并不相等
func TestErrString(t *testing.T) { var error1 = errors.New("error") var error2 = errors.New("error") if error1 != error2 { log.Println("error1 != error2") } } ---------------------代碼運(yùn)行結(jié)果-------------------------- === RUN TestXXXX 2022/03/25 22:05:40 error1 != error2
參考文獻(xiàn)
《Effective GO》
《Go程序設(shè)計(jì)語(yǔ)言》
https://dave.cheney.net/practical-go/presentations/qcon-china.html#_error_handling
總結(jié)
到此這篇關(guān)于Golang中error處理的文章就介紹到這了,更多相關(guān)Golang error處理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何組織Go代碼目錄結(jié)構(gòu)依賴注入wire使用解析
這篇文章主要為大家介紹了如何組織Go代碼目錄結(jié)構(gòu)依賴注入wire使用解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07Go語(yǔ)言繼承功能使用結(jié)構(gòu)體實(shí)現(xiàn)代碼重用
今天我來(lái)給大家介紹一下在?Go?語(yǔ)言中如何實(shí)現(xiàn)類似于繼承的功能,讓我們的代碼更加簡(jiǎn)潔和可重用,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01