大家好,我是渔夫子。今天从应用场景的角度来聊聊我对error的理解。01 什么是Error在Go中,error是一种内建的数据类型,被定义为一个接口,定义如下: // 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函数,所有的类型只要实现了该函数,就创建了一个错误类型。 02 创建error的方式创建error的方式包括errors.New、fmt.Errorf、自定义实现了error接口的类型等。 2.1 通过errors.New方法创建 通过该方法创建的错误一般是可预知的错误。简单来说就是调用者通过该错误信息就能明确的知道哪里出错了,而不需要再额外的添加其他上下文信息,我们在下面的示例中详细说明。 err := errors.New('this is error')
我们看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 }
error.New 使用场景1 通过errors.New函数创建局部变量或匿名变量,且不在调用函数中进行值或类型判断的处理,只打印或记录错误日志的场景。 使用示例1 以下代码节选自源码/src/net/http/request.go中解析PostForm的部分。当请求中的Body为nil时,返回的错误信息是'missing form body'。该信息已明确的说明错误是因为请求体为空造成的,所以不需要再额外的添加其他上下文信息。 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') // 省略了后续的代码... return }
使用示例2 以下代码节选自源码/src/net/http/transport.go的部分,当请求体中的url地址为nil返回的错误:'http: nil Request.URL' ,说明是请求中的URL字段为nil。以及当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创建的错误赋值给一个全局的变量,我们称该变量为哨兵错误,该哨兵错误变量可以在被处理的时候使用 == 或 errors.Is来进行值的比较。 使用示例 在源码/src/io/io.go中定义的代表文件末尾的哨兵错误变量EOF。 var EOF = errors.New('EOF')
在beego项目中,beego/core/utils/file.go文件中有这样的应用,当读取文件时,遇到的错误不是文件末尾的错误则直接返回,如果遇到的是文件末尾的错误,则中断for循环,说明文件已经读完文件中的所有内容了。如下: 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方法创建 该方法也有两种形式,一种是带%w占位符的,一种是不带%w占位符的方式。 使用场景1:不带%w占位符 在创建错误的时候,不能通过errors.New创建的字符串信息来描述错误,而需要通过占位符添加更多的上下文信息,即动态信息。 使用示例:不带%w占位符 以下代码节选自gorm/schema/relationship.go的部分代码,当外键不合法时,通过fmt.Errorf('invalid foreign key:%s', foreignKey)返回带具体外键的错误。因为外键值是在运行时才能确定的。代码如下: 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占位符 在有些场景下,调用者需要知道原始错误信息,这时就需要使用带%w占位符的fmt.Errorf方式来创建错误,使用这种方式,其实是形成了一个错误链。 其用法如下: 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占位符,创建的是一个wrapError结构体类型的值。我们再来看下wrapError结构体的定义: type wrapError struct { msg string err error }
字段err就是原始错误,msg是经过格式化之后的错误信息。 使用示例:带%w占位符 假设我们有一个从数据库查询合同的函数,当从数据库中查询到记录为空时,会返回一个sql.ErrNoRows错误,我们用%w占位符来wrap该错误,并返回给调用者。 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) } // ... } // ... }
好了,现在GetContract的调用者可以知道原始的错误信息了。在调用者逻辑中我们可以使用errors.Is来判断err中是否包含sql.ErrNoRows值了。我们看下调用者的代码: contract, err := store.GetContract('Raul Endymion') if err != nil { if errors.Is(err, sql.ErrNoRows) { // Do something specific } }
2.3 自定义实现了error接口的结构体 使用场景 这个是相对errors.New来说的,errors.New适用于对可预知的错误的定义。而当发生了不可预知的错误时,就需要自定义错误类型了。 使用示例 我们以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 }
首先看结构体,有一个error接口类型的Err,这个代表的是错误源,因为根据上面讲解的,在错误层层传递返回给调用者时,我们需要追踪每一层的原始错误信息,所以需要该字段对error进行wrap,形成错误链。另外,有两个字段Op和Path,分别代表是产生该错误的操作和操作的路径。这两个字段就是所谓的未预料到的错误:不确定是针对哪个路径做了什么错误引发了该错误。 我们看下该错误类型在代码中的应用。 应用1:在go的文件src/embed/embed.go中的代码,当读取某目录时返回的一个PathError类型的错误,代表读取该目录操作时,因为是一个目录,所以不能直接读取文件内容。 func (d *openDir) Read([]byte) (int, error) { return 0, &fs.PathError{Op: 'read', Path: d.f.name, Err: errors.New('is a directory')} }
应用2:在go的文件src/embed/embed.go中的代码中,有文件读取的函数,当offset小于0时,返回了一个PathError,代表是在读取该文件的时候,参数不正确。 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中的三个字段值都是不可预知的,都需要在程序运行时才能具体决定的,所以这种场景时,则需要自定义错误类型。 另外,我们还注意到该自定义的类型中有Unwrap函数的实现,该函数主要是为了配合errors.Is和errors.As使用的,因为这两个函数在使用时是将错误链层层解包一一比对的。 03 errors.Is和errors.As根据上一节我们得到,通过%w占位符可以将错误组织成一个错误链。我们再来看看通过errors.Is和errors.As如何处理被wrap过的错误链。 errors.Is函数就是来判断错误链中有没有和指定的错误值相等的错误,相当于 == 操作符。注意,这里是特定的错误值,就像gorm中定义的ErrRecordNotFound这样: var ErrRecordNotFound = errors.New('record not found')
那么我们就可以这样使用errors.Is: errors.Is(err, ErrRecordNotFound)
errors.As函数,该函数是用来检查错误链中的错误是否有指定的错误类型的。 如下代码示例是节选自etcd项目etcd/server/embed/config_logging.go中的部分代码,代表的是err链中有没有能当做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 }
总结本文从应用场景的角度讲解了各种创建错误方式的实际应用场景。示例中的代码尽量的选自golang源码或开源项目。同时,每种的应用场景并非绝对的,需要灵活应用。希望本文对大家在实际使用中能够有所帮助。
|