Go进阶之异常处理error

18 阅读6分钟

1.error接口:

erorr是一种内建的接口类型.内建意味着不需要"import".任何包都可以直接使用,

使用起来就像int string一样自然.

源码位置:src/builtin/builtin.go

// 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接口只声明了一个Error()方法.任何实现了该方法的结构体都可以

作为error来使用.error的实例代表一种异常状态.Error()方法用于描述该异常状态.

值为nil的error代表没有异常.

标准库erroor包中的errorString就是实现error接口的一个例子.

源码位置:src/errors/errors.go

type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

errorString是errors包的私有类型.对外不可见.只能通过相应的公开接口才可以创

建errorString实例.

2.创建error:

标准库创建方法:

1).errors.New():

源码位置:src/errors/errors.go

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
    return &errorString{text}
}

errors.New()实现比较简单.只是单纯的构建了一个errorString实例便返回了.

2).fmt.Errorf():

源码位置:src/fmt/errors.go

func Errorf(format string, a ...any) error {
    p := newPrinter()
    p.wrapErrs = true
    p.doPrintf(format, a)
    s := string(p.buf)
    var err error
    switch len(p.wrappedErrs) {
    case 0:
       err = errors.New(s)
    case 1:
       w := &wrapError{msg: s}
       w.err, _ = a[p.wrappedErrs[0]].(error)
       err = w
    default:
       if p.reordered {
          slices.Sort(p.wrappedErrs)
       }
       var errs []error
       for i, argNum := range p.wrappedErrs {
          if i > 0 && p.wrappedErrs[i-1] == argNum {
             continue
          }
          if e, ok := a[argNum].(error); ok {
             errs = append(errs, e)
          }
       }
       err = &wrapErrors{s, errs}
    }
    p.free()
    return err
}

fmt.Errorf会接受两个参数然后对string进行格式化.

3.性能对比:

fmt.Errorf()适用于格式化输出错误字符串的场景.如果不需要格式化字符串.则建议

直接使用errors.New().

示例如下:

package Concurrent

import (
    "errors"
    "fmt"
    "testing"
)

// 场景1:无格式化参数的简单错误(errors.New 原生场景 vs fmt.Errorf 无参数)
func BenchmarkErrorsNew_Simple(b *testing.B) {
    // 重置计时器,排除初始化耗时
    b.ResetTimer()
    // 循环执行 b.N 次,b.N 由基准测试框架自动调整
    for i := 0; i < b.N; i++ {
       _ = errors.New("simple error")
    }
}

func BenchmarkFmtErrorf_Simple(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
       // fmt.Errorf 无格式化参数,等价于 errors.New
       _ = fmt.Errorf("simple error")
    }
}

// 场景2:带格式化参数的错误(fmt.Errorf 核心场景,errors.New 无法直接实现)
func BenchmarkFmtErrorf_Format(b *testing.B) {
    // 测试参数:字符串+数字,模拟真实业务格式化场景
    msg := "user"
    uid := 1001
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
       _ = fmt.Errorf("user %s not found, uid: %d", msg, uid)
    }
}

// 场景3:带错误包装的场景(%w 动词,fmt.Errorf 特有)
func BenchmarkFmtErrorf_Wrap(b *testing.B) {
    baseErr := errors.New("base error")
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
       _ = fmt.Errorf("wrap error: %w", baseErr)
    }
}

4.自定义error:

任何实现error接口的类型都可以称为error.比如标准库os中的PathError就是一个

典型的例子.

源码位置:src/io/fs.go

type PathError struct {
    Op   string
    Path string
    Err  error
}

func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }

5.异常处理:

针对error而言.异常处理包括如何检查错误 如何传递错误.

1).检查error:

最常见的检查error的方式是与nil值进行比较:

func main() {

    var err error = T(5)

    if err != nil {
       
    }
  
}

有时也会与一些预定义的error进行比较:

package oserror

import "errors"

var (
    ErrInvalid    = errors.New("invalid argument")
    ErrPermission = errors.New("permission denied")
    ErrExist      = errors.New("file already exists")
    ErrNotExist   = errors.New("file does not exist")
    ErrClosed     = errors.New("file already closed")
)

实现了error接口的类型均可以作为error来处理.也可以使用类型断言来检查error.

func AssertError(err error) {
    if e,ok := err.(*os.PathError); ok {
       fmt.Printf("path error: %s", e.Path)
    }
}

2).传递error:

在一个函数中收到一个error.往往需要附加一些上下文信息再把error继续往上抛.

最常见的添加附加上下文信息的方法是用fmt.Errorf().

func ReturnErrorTest() {
    err := errors.New("test error")
    if err != nil {
       fmt.Errorf("我定义的异常进行了包装%s", err)
    }
}

这种方式抛出的error有一个糟糕的问题.就是原error信息和附加的信息被糅合到了

一起.示例如下:

```
import (
    "fmt"
    "os"
)

func WriteFile(fileName string) error {
    if fileName == "测试.txt" {
       return fmt.Errorf("write file error:%v", os.ErrPermission)
    }
    return nil
}

func ExampleWriteFile() {
    err := WriteFile("测试.txt")
    if err == os.ErrPermission {
       fmt.Printf("write file error:%v", os.ErrPermission)
    }
}
```

在这个例子中.无法明确到底是不是这个异常.为了解决这个问题.可以自定义error类

型.就像os.PathError.上下文与信息分开放.

type PathError struct {
    Op   string  //上下文
    Path string  //上下文
    Err  error   //原error
}

6.wrapError:

Go1.13针对error的优化.最核心的内容就是引入了wrapError这一新的error类型.

其他特性都是围绕此类型展开的.

源码位置:src/fmt/errors.go:

type wrapError struct {
    msg string
    err error
}

func (e *wrapError) Error() string {
    return e.msg
}

func (e *wrapError) Unwrap() error {
    return e.err
}

wrapError中的msg保存上下文信息和err.Error(). err用来存储原error.与之前的

errorString相比.还额外实现了Unwrap接口.用于返回原始的error.

7.fmt.Errorf():

fmt.Errorf()新增了格式动词%w(wrap)用于生成wrapError实例.并且兼容原有动

词格式.源码如下:

func Errorf(format string, a ...any) error {
    p := newPrinter()
    p.wrapErrs = true
    p.doPrintf(format, a)
    s := string(p.buf)
    var err error
    switch len(p.wrappedErrs) {
    case 0:
       err = errors.New(s)
    case 1:
       w := &wrapError{msg: s}
       w.err, _ = a[p.wrappedErrs[0]].(error)
       err = w
    default:
       if p.reordered {
          slices.Sort(p.wrappedErrs)
       }
       var errs []error
       for i, argNum := range p.wrappedErrs {
          if i > 0 && p.wrappedErrs[i-1] == argNum {
             continue
          }
          if e, ok := a[argNum].(error); ok {
             errs = append(errs, e)
          }
       }
       err = &wrapErrors{s, errs}
    }
    p.free()
    return err
}

fmt.Errorf()将根据动词格式来动态决定生成wrapError还是errorString.

func main() {

    err := errors.New("this is an error")
    //使用%v
    baseError := fmt.Errorf("this is a %v", err)
    if _, ok := baseError.(interface{ Unwrap() error }); !ok {
       fmt.Println("baseError is errorString")
    }
}

使用%w格式动词生成的error类型自动变成wrapError(实现了Unwrap接口).

func main() {
    err := errors.New("this is an error")
    //使用%w
    baseError := fmt.Errorf("this is a %w", err)
    if _, ok := baseError.(interface{ Unwrap() error }); ok {
       fmt.Println("baseError is wrapError")
    }
}

当error在函数间传递时.error之间好像被组织成一个链式结构.

8.errors.Unwrap():

源码位置:src/errors/wrap.go

func Unwrap(err error) error {
    u, ok := err.(interface {
       Unwrap() error
    })
    if !ok {
       return nil
    }
    return u.Unwrap()
}

如果参数err没有实现Unwrap()函数.则说明是基础error.直接返回nil.否则调用原

error实现的Unwrap函数并返回. 可以通过循环调用errors.Unwrap()方法来逐层

检查.示例如下:

func ExampleUnwrapLoop() {
    err1 := fmt.Errorf("write file error:%w", os.ErrPermission)
    err2 := fmt.Errorf("write file error:%w", err1)
    err := err2
    for {
       if err == os.ErrPermission {
          fmt.Printf("Permission denied\n")
          break
       }
       if err = errors.Unwrap(err); err == nil {
          break
       }
    }
}

9.errors.Is():

errors.Is()用于检查特定的error链中是否包含指定的error值.

源码位置:src/errors/wrap.go

func Is(err, target error) bool {
    if err == nil || target == nil {
       return err == target
    }

    isComparable := reflectlite.TypeOf(target).Comparable()
    return is(err, target, isComparable)
}

image.png

func is(err, target error, targetComparable bool) bool {
    for {
       if targetComparable && err == target {
          return true
       }
       if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
          return true
       }
       switch x := err.(type) {
       case interface{ Unwrap() error }:
          err = x.Unwrap()
          if err == nil {
             return false
          }
       case interface{ Unwrap() []error }:
          for _, err := range x.Unwrap() {
             if is(err, target, targetComparable) {
                return true
             }
          }
          return false
       default:
          return false
       }
    }
}

10.errors.As()方法:

在Go1.13中.errors.As()用于从一个error链中查找是否有指定类型出现.如有.则把

error转换成该类型.

源码位置:src/errors/wrap.go

func As(err error, target any) bool {
    if err == nil {
       return false
    }
    if target == nil {
       panic("errors: target cannot be nil")
    }
    val := reflectlite.ValueOf(target)
    typ := val.Type()
    if typ.Kind() != reflectlite.Ptr || val.IsNil() {
       panic("errors: target must be a non-nil pointer")
    }
    targetType := typ.Elem()
    if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) {
       panic("errors: *target must be interface or implement error")
    }
    return as(err, target, val, targetType)
}

image.png

func as(err error, target any, targetVal reflectlite.Value, targetType reflectlite.Type) bool {
    for {
       if reflectlite.TypeOf(err).AssignableTo(targetType) {
          targetVal.Elem().Set(reflectlite.ValueOf(err))
          return true
       }
       if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) {
          return true
       }
       switch x := err.(type) {
       case interface{ Unwrap() error }:
          err = x.Unwrap()
          if err == nil {
             return false
          }
       case interface{ Unwrap() []error }:
          for _, err := range x.Unwrap() {
             if err == nil {
                continue
             }
             if as(err, target, targetVal, targetType) {
                return true
             }
          }
          return false
       default:
          return false
       }
    }
}

image.png

山高路远.





如果大家喜欢我的分享的话.可以关注我的微信公众号

念何架构之路