<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
Golang中的 error 就是一個簡單的介面型別。只要實現了這個介面,就可以將其視為一種 error
type error interface { Error() string }
翻看Golang原始碼,能看到許多類似於下面的這兩種error型別
var EOF = errors.New("EOF") var ErrUnexpectedEOF = errors.New("unexpected EOF") var ErrNoProgress = errors.New("multiple Read calls return no data or error")
缺點:
1.讓 error 具有二義性
error != nil不再意味著一定發生了錯誤
比如io.Reader返回io.EOF來告知呼叫者沒有更多資料了,然而這又不是一個錯誤
2.在兩個包之間建立了依賴
如果你使用了io.EOF來檢查是否read完所有的資料,那麼程式碼裡一定會匯入io包
一個不錯的例子是os.PathError,它的優點是可以附帶更多的上下文資訊
type PathError struct { Op string Path string Err error }
到這裡我們可以發現,Golang 的 error 非常簡單,然而簡單也意味著有時候是不夠用的
Golang的error一直有兩個問題:
1.error沒有附帶file:line資訊(也就是沒有堆疊資訊)
比如這種error,鬼知道程式碼哪一行報了錯,Debug時簡直要命
SERVICE ERROR 2022-03-25T16:32:10.687+0800!!!
Error 1406: Data too long for column 'content' at row 1
2.上層error想附帶更多紀錄檔資訊時,往往會使用fmt.Errorf()
,fmt.Errorf()
會建立一個新的error,底層的error型別就被“吞”掉了
var errNoRows = errors.New("no rows") // 模仿sql庫返回一個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建立了一個新的err,丟失了底層err err := serviceNoErrWrap() if err != errNoRows { log.Println("===== errType don't equal errNoRows =====") } } -------------------------------程式碼執行結果---------------------------------- === RUN TestErrWrap 2022/03/26 17:19:43 ===== errType don't equal errNoRows =====
為了解決這個問題,我們可以使用github.com/pkg/error包
,使用errors.withStack()方法
將err保
存到withStack物件
// withStack結構體儲存了error,形成了一條error鏈。同時*stack欄位儲存了堆疊資訊。 type withStack struct { error *stack }
也可以使用errors.Wrap(err, "自定義文字")
,額外附帶一些自定義的文字資訊
原始碼解讀:先將err和message包進withMessage物件
,再將withMessage物件
和堆疊資訊包進withStack物件
func Wrap(err error, message string) error { if err == nil { return nil } err = &withMessage{ cause: err, msg: message, } return &withStack{ err, callers(), } }
Golang1.13版本借鑑了github.com/pkg/error包
,新增瞭如下函數,大大增強了 Golang 語言判斷 error 型別的能力
// 與errors.Wrap()行為相反 // 獲取err鏈中的底層err func Unwrap(err error) error { u, ok := err.(interface { Unwrap() error }) if !ok { return nil } return u.Unwrap() }
在1.13版本之前,我們可以用err == targetErr
判斷err型別errors.Is()
是其增強版:error 鏈上的任一err == targetErr
,即return true
// 實踐:學習使用errors.Is() var errNoRows = errors.New("no rows") // 模仿sql庫返回一個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遞迴呼叫errors.UnWrap,命中err鏈上的任意err即返回true if errors.Is(err, errNoRows) { log.Println("===== errors.Is() succeeded =====") } //err經errors.WithStack包裝,不能通過 == 判斷err型別 if err == errNoRows { log.Println("err == errNoRows") } } -------------------------------程式碼執行結果---------------------------------- === RUN TestErrIs 2022/03/25 18:35:00 ===== errors.Is() succeeded =====
例子解讀:
因為使用errors.WithStack
包裝了sqlError
,sqlError
位於error鏈的底層,上層的error已經不再是sqlError
型別,所以使用==
無法判斷出底層的sqlError
原始碼解讀:
err = Unwrap(err)
方法來獲取error鏈中底層的errorIs介面
來自定義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 } } }
下面我們來看看如何自定義error型別判斷:
自定義的errNoRows型別
,必須實現Is介面
,才能使用erros.Is()
進行型別判斷
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庫返回一個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 =====") } } -------------------------------程式碼執行結果---------------------------------- === RUN TestErrIs 2022/03/25 18:35:00 ===== errors.Is() succeeded =====
在1.13版本之前,我們可以用if _,ok := err.(targetErr)
判斷err型別errors.As()
是其增強版:error 鏈上的任一err與targetErr型別相同
,即return true
// 通過例子學習使用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 } // 返回一個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 =====") } // 經errors.WithStack包裝後,不能通過型別斷言將當前Err轉換成底層Err if _, ok := err.(sqlError); ok { log.Println("===== type assert succeeded =====") } } ----------------------------------程式碼執行結果-------------------------------------------- === RUN TestErrAs 2022/03/25 18:09:02 ===== errors.As() succeeded =====
例子解讀:
因為使用errors.WithStack
包裝了sqlError
,sqlError
位於error鏈的底層,上層的error已經不再是sqlError
型別,所以使用型別斷言無法判斷出底層的sqlError
上面講了如何定義error型別,如何比較error型別,現在我們談談如何在大型專案中做好error處理
當一個函數返回一個非空error時,應該優先處理error,忽略它的其他返回值
比如下面例子json.Marshal(conf)沒有return err ,那麼在使用buf時一定要小心空指標等錯誤
要麼return err,在上層處理err
反例:
// 試想如果writeAll函數出錯,會列印兩遍紀錄檔 // 如果整個專案都這麼做,最後會驚奇的發現我們在處處打紀錄檔,專案中存在大量沒有價值的垃圾紀錄檔 // 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 }
我們應該包裝error,但只包裝一次
上層業務程式碼建議Wrap error
,但是底層基礎Kit庫不建議
如果底層基礎 Kit 庫包裝了一次,上層業務程式碼又包裝了一次,就重複包裝了 error,紀錄檔就會打重
比如我們常用的sql庫
會返回sql.ErrNoRows
這種預定義錯誤,而不是給我們一個包裝過的 error
在大型專案中,推薦使用不透明的錯誤處理(Opaque errors)
:不關心錯誤型別,只關心error是否為nil
好處:
耦合小,不需要判斷特定錯誤型別,就不需要匯入相關包的依賴。
不過有時候,這種處理error的方式不夠用,比如:業務需要對引數異常error型別
做降級處理,列印Warn級別的紀錄檔
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) }
Golang因為程式碼中無數的if err != nil
被詬病,現在我們看看如何減少if err != nil
這種程式碼
CountLines() 實現了"讀取內容的行數"功能
可以利用 bufio.scan() 簡化 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()
返回一個 Scanner
物件,結構體內部包含了 error 型別,呼叫Err()
方法即可返回封裝好的error
Golang原始碼中蘊含著大量的優秀設計思想,我們在閱讀原始碼時從中學習,並在實踐中得以運用
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 }
WriteResponse()
函數實現了"構建HttpResponse"
功能
利用上面學到的思路,我們可以自己實現一個errWriter
物件,簡化對 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 %srn", st.Code, st.Reason) if err != nil { return err } for _, h := range headers { _, err := fmt.Fprintf(w, "%s: %srn", h.Key, h.Value) if err != nil { return err } } if _, err := fmt.Fprintf(w, "rn"); 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 %srn", st.Code, st.Reason) for _, h := range headers { fmt.Fprintf(ew, "%s: %srn", h.Key, h.Value) } fmt.Fprintf(w, "rn") io.Copy(ew, body) return ew.err }
在 Golang 中panic
會導致程式直接退出,是一個致命的錯誤。
建議發生致命的程式錯誤時才使用 panic,例如索引越界、不可恢復的環境問題、棧溢位等等
errors.New()
返回的是errorString物件
的指標,其原因是防止字串產生碰撞,如果發生碰撞,兩個 error 物件會相等。
原始碼:
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 }
實踐: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") } } ---------------------程式碼執行結果-------------------------- === RUN TestXXXX 2022/03/25 22:05:40 error1 != error2
參考文獻
《Effective GO》
《Go程式設計語言》
https://dave.cheney.net/practical-go/presentations/qcon-china.html#_error_handling
到此這篇關於Golang中error處理的文章就介紹到這了,更多相關Golang error處理內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45