詳解Golang中創(chuàng)建error的方式總結(jié)與應(yīng)用場景
01 什么是Error
在Go中,error是一種內(nèi)建的數(shù)據(jù)類型,被定義為一個接口,定義如下:
// The error built-in interface type is the conventional interface for // representing an error condition, with the nil value representing no error. type error interface { Error() string }
由此可知,該接口只有一個返回字符串的Error函數(shù),所有的類型只要實(shí)現(xiàn)了該函數(shù),就創(chuàng)建了一個錯誤類型。
02 創(chuàng)建error的方式
創(chuàng)建error的方式包括errors.New、fmt.Errorf、自定義實(shí)現(xiàn)了error接口的類型等。
2.1 通過errors.New方法創(chuàng)建
通過該方法創(chuàng)建的錯誤一般是可預(yù)知的錯誤。簡單來說就是調(diào)用者通過該錯誤信息就能明確的知道哪里出錯了,而不需要再額外的添加其他上下文信息,我們在下面的示例中詳細(xì)說明。
err := errors.New("this is error")
我們看New方法的實(shí)現(xiàn)可知,實(shí)際上是返回了一個errorString結(jié)構(gòu)體,該結(jié)構(gòu)體包含了一個字符串屬性,并實(shí)現(xiàn)了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 }
error.New 使用場景1
通過errors.New函數(shù)創(chuàng)建局部變量或匿名變量,且不在調(diào)用函數(shù)中進(jìn)行值或類型判斷的處理,只打印或記錄錯誤日志的場景。
使用示例1
以下代碼節(jié)選自源碼/src/net/http/request.go中解析PostForm的部分。當(dāng)請求中的Body為nil時,返回的錯誤信息是"missing form body"。該信息已明確的說明錯誤是因?yàn)檎埱篌w為空造成的,所以不需要再額外的添加其他上下文信息。
func parsePostForm(r *Request) (vs url.Values, err error) { if r.Body == nil { err = errors.New("missing form body") return } ct := r.Header.Get("Content-Type") // 省略了后續(xù)的代碼... return }
使用示例2
以下代碼節(jié)選自源碼/src/net/http/transport.go的部分,當(dāng)請求體中的url地址為nil返回的錯誤:"http: nil Request.URL" ,說明是請求中的URL字段為nil。以及當(dāng)Header為nil返回的錯誤:"http:nil Request.Header",說明請求體中的Header字段為nil。
func (t *Transport) roundTrip(req *Request) (*Response, error) { t.nextProtoOnce.Do(t.onceSetNextProtoDefaults) ctx := req.Context() trace := httptrace.ContextClientTrace(ctx) if req.URL == nil { req.closeBody() return nil, errors.New("http: nil Request.URL") } if req.Header == nil { req.closeBody() return nil, errors.New("http: nil Request.Header") } //省略后面的代碼... }
error.New 使用場景2
將errors.New創(chuàng)建的錯誤賦值給一個全局的變量,我們稱該變量為哨兵錯誤,該哨兵錯誤變量可以在被處理的時候使用 == 或 errors.Is來進(jìn)行值的比較。
使用示例
在源碼/src/io/io.go中定義的代表文件末尾的哨兵錯誤變量EOF。
var EOF = errors.New("EOF")
在beego項(xiàng)目中,beego/core/utils/file.go文件中有這樣的應(yīng)用,當(dāng)讀取文件時,遇到的錯誤不是文件末尾的錯誤則直接返回,如果遇到的是文件末尾的錯誤,則中斷for循環(huán),說明文件已經(jīng)讀完文件中的所有內(nèi)容了。如下:
func GrepFile(patten string, filename string) (lines []string, err error) { //省略前面的代碼... fd, err := os.Open(filename) if err != nil { return } reader := bufio.NewReader(fd) for { byteLine, isPrefix, er := reader.ReadLine() if er != nil && er != io.EOF { return nil, er } if er == io.EOF { break } //省略后面的代碼... }
2.2 通過fmt.Errorf方法創(chuàng)建
該方法也有兩種形式,一種是帶%w占位符的,一種是不帶%w占位符的方式。
使用場景1:不帶%w占位符
在創(chuàng)建錯誤的時候,不能通過errors.New創(chuàng)建的字符串信息來描述錯誤,而需要通過占位符添加更多的上下文信息,即動態(tài)信息。
使用示例:不帶%w占位符
以下代碼節(jié)選自gorm/schema/relationship.go的部分代碼,當(dāng)外鍵不合法時,通過fmt.Errorf("invalid foreign key:%s", foreignKey)返回帶具體外鍵的錯誤。因?yàn)橥怄I值是在運(yùn)行時才能確定的。代碼如下:
func (schema *Schema) buildMany2ManyRelation(relation *Relationship, field *Field, many2many string) { //... if len(relation.foreignKeys) > 0 { ownForeignFields = []*Field{} for _, foreignKey := range relation.foreignKeys { if field := schema.LookUpField(foreignKey); field != nil { ownForeignFields = append(ownForeignFields, field) } else { schema.err = fmt.Errorf("invalid foreign key: %s", foreignKey) return } } } //... }
使用場景2:帶%w占位符
在有些場景下,調(diào)用者需要知道原始錯誤信息,這時就需要使用帶%w占位符的fmt.Errorf方式來創(chuàng)建錯誤,使用這種方式,其實(shí)是形成了一個錯誤鏈。
其用法如下:
filename := "abc.webp" fmt.Errorf("%w:%s", errors.New("unsupported extension"), filename)
我們再來看下源代碼:
func Errorf(format string, a ...interface{}) error { p := newPrinter() p.wrapErrs = true p.doPrintf(format, a) s := string(p.buf) var err error if p.wrappedErr == nil { err = errors.New(s) } else { err = &wrapError{s, p.wrappedErr} } p.free() return err }
通過源碼可知,如果fmt.Errorf中包含%w占位符,創(chuàng)建的是一個wrapError結(jié)構(gòu)體類型的值。我們再來看下wrapError結(jié)構(gòu)體的定義:
type wrapError struct { msg string err error }
字段err就是原始錯誤,msg是經(jīng)過格式化之后的錯誤信息。
使用示例:帶%w占位符
假設(shè)我們有一個從數(shù)據(jù)庫查詢合同的函數(shù),當(dāng)從數(shù)據(jù)庫中查詢到記錄為空時,會返回一個sql.ErrNoRows錯誤,我們用%w占位符來wrap該錯誤,并返回給調(diào)用者。
const query = "..." func (s Store) GetContract(name string) (Contract, error) { id := getID(name) rows, err := s.db.Query(query, id) if err != nil { if err == sql.ErrNoRows { return Contract{}, fmt.Errorf("no contract found for %s: %w", name, err) } // ... } // ... }
好了,現(xiàn)在GetContract的調(diào)用者可以知道原始的錯誤信息了。在調(diào)用者邏輯中我們可以使用errors.Is來判斷err中是否包含sql.ErrNoRows值了。我們看下調(diào)用者的代碼:
contract, err := store.GetContract("Raul Endymion") if err != nil { if errors.Is(err, sql.ErrNoRows) { // Do something specific } }
2.3 自定義實(shí)現(xiàn)了error接口的結(jié)構(gòu)體
使用場景
這個是相對errors.New來說的,errors.New適用于對可預(yù)知的錯誤的定義。而當(dāng)發(fā)生了不可預(yù)知的錯誤時,就需要自定義錯誤類型了。
使用示例
我們以go中/src/io/fs/fs.go文件中的源碼為例,來看下自定義錯誤類型都需要包含哪些元素。
// PathError records an error and the operation and file path that caused it. type PathError struct { Op string Path string Err error } func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() } func (e *PathError) Unwrap() error { return e.Err }
首先看結(jié)構(gòu)體,有一個error接口類型的Err,這個代表的是錯誤源,因?yàn)楦鶕?jù)上面講解的,在錯誤層層傳遞返回給調(diào)用者時,我們需要追蹤每一層的原始錯誤信息,所以需要該字段對error進(jìn)行wrap,形成錯誤鏈。另外,有兩個字段Op和Path,分別代表是產(chǎn)生該錯誤的操作和操作的路徑。這兩個字段就是所謂的未預(yù)料到的錯誤:不確定是針對哪個路徑做了什么錯誤引發(fā)了該錯誤。
我們看下該錯誤類型在代碼中的應(yīng)用。
應(yīng)用1:在go的文件src/embed/embed.go中的代碼,當(dāng)讀取某目錄時返回的一個PathError類型的錯誤,代表讀取該目錄操作時,因?yàn)槭且粋€目錄,所以不能直接讀取文件內(nèi)容。
func (d *openDir) Read([]byte) (int, error) { return 0, &fs.PathError{Op: "read", Path: d.f.name, Err: errors.New("is a directory")} }
應(yīng)用2:在go的文件src/embed/embed.go中的代碼中,有文件讀取的函數(shù),當(dāng)offset小于0時,返回了一個PathError,代表是在讀取該文件的時候,參數(shù)不正確。
func (f *openFile) Read(b []byte) (int, error) { if f.offset >= int64(len(f.f.data)) { return 0, io.EOF } if f.offset < 0 { return 0, &fs.PathError{Op: "read", Path: f.f.name, Err: fs.ErrInvalid} } n := copy(b, f.f.data[f.offset:]) f.offset += int64(n) return n, nil }
fs.ErrInvalid的定義如下
ErrInvalid = errors.New("invalid argument")
由此可見,PathError中的三個字段值都是不可預(yù)知的,都需要在程序運(yùn)行時才能具體決定的,所以這種場景時,則需要自定義錯誤類型。
另外,我們還注意到該自定義的類型中有Unwrap函數(shù)的實(shí)現(xiàn),該函數(shù)主要是為了配合errors.Is和errors.As使用的,因?yàn)檫@兩個函數(shù)在使用時是將錯誤鏈層層解包一一比對的。
03 errors.Is和errors.As
根據(jù)上一節(jié)我們得到,通過%w占位符可以將錯誤組織成一個錯誤鏈。我們再來看看通過errors.Is和errors.As如何處理被wrap過的錯誤鏈。
errors.Is函數(shù)就是來判斷錯誤鏈中有沒有和指定的錯誤值相等的錯誤,相當(dāng)于 == 操作符。注意,這里是特定的錯誤值,就像gorm中定義的ErrRecordNotFound這樣:
var ErrRecordNotFound = errors.New("record not found")
那么我們就可以這樣使用errors.Is:
errors.Is(err, ErrRecordNotFound)
errors.As函數(shù),該函數(shù)是用來檢查錯誤鏈中的錯誤是否有指定的錯誤類型的。
如下代碼示例是節(jié)選自etcd項(xiàng)目etcd/server/embed/config_logging.go中的部分代碼,代表的是err鏈中有沒有能當(dāng)做json.SyntaxError類型的錯誤的,如果能,則將err中的錯誤值賦值到syntaxError變量上,代碼如下:
// setupLogRotation initializes log rotation for a single file path target. func setupLogRotation(logOutputs []string, logRotateConfigJSON string) error { //... if err := json.Unmarshal([]byte(logRotateConfigJSON), &logRotationConfig); err != nil { var unmarshalTypeError *json.UnmarshalTypeError var syntaxError *json.SyntaxError switch { case errors.As(err, &syntaxError): return fmt.Errorf("improperly formatted log rotation config: %w", err) case errors.As(err, &unmarshalTypeError): return fmt.Errorf("invalid log rotation config: %w", err) } } zap.RegisterSink("rotate", func(u *url.URL) (zap.Sink, error) { logRotationConfig.Filename = u.Path[1:] return &logRotationConfig, nil }) return nil }
總結(jié)
本文從應(yīng)用場景的角度講解了各種創(chuàng)建錯誤方式的實(shí)際應(yīng)用場景。示例中的代碼盡量的選自golang源碼或開源項(xiàng)目。同時,每種的應(yīng)用場景并非絕對的,需要靈活應(yīng)用。希望本文對大家在實(shí)際使用中能夠有所幫助。
以上就是詳解Golang中創(chuàng)建error的方式總結(jié)與應(yīng)用的詳細(xì)內(nèi)容,更多關(guān)于Golang創(chuàng)建error的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
在ubuntu下安裝go開發(fā)環(huán)境的全過程
Go語言是谷歌公司開發(fā)的編程語言,雖然安裝和配置go很簡單,但是很多初學(xué)者在第一次安裝go環(huán)境時會遇到各種坑,下面這篇文章主要給大家介紹了關(guān)于在ubuntu下安裝go開發(fā)環(huán)境的相關(guān)資料,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2022-08-08Golang對struct字段重新排序優(yōu)化數(shù)據(jù)結(jié)構(gòu)性能實(shí)踐
這篇文章主要為大家介紹了Golang對struct字段重新排序優(yōu)化數(shù)據(jù)結(jié)構(gòu)性能實(shí)踐,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12深入分析golang多值返回以及閉包的實(shí)現(xiàn)
相對于C/C++,golang有很多新穎的特性,例如goroutine,channel等等,這些特性其實(shí)從golang源碼是可以理解其實(shí)現(xiàn)的原理。今天這篇文章主要來分析下golang多值返回以及閉包的實(shí)現(xiàn),因?yàn)檫@兩個實(shí)現(xiàn)golang源碼中并不存在,我們必須從匯編的角度來窺探二者的實(shí)現(xiàn)。2016-09-09詳解Go語言中的數(shù)據(jù)類型及類型轉(zhuǎn)換
這篇文章主要為大家介紹了Go語言中常見的幾種數(shù)據(jù)類型,以及他們之間的轉(zhuǎn)換方法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2022-04-04golang?xorm?自定義日志記錄器之使用zap實(shí)現(xiàn)日志輸出、切割日志(最新)
這篇文章主要介紹了golang?xorm?自定義日志記錄器,使用zap實(shí)現(xiàn)日志輸出、切割日志,包括連接postgresql數(shù)據(jù)庫的操作方法及?zap日志工具?,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-10-10