# 4.2 错误值检查
我们先来看第一个问题:如何对一个传播链条中的错误类型进行断言?
在标准库中,`errors`包中最为重要的一个`New`函数能够从给定格式的字符串中创建一个错误, 它的内部实现仅仅是对`error`接口的一个实现`errorString`:
```
package errors
type errorString struct { s string }
func (e *errorString) Error() string { return e.s }
func New(text string) error { return &errorString{text} }
```
当然,这远远不够。为了能够对错误进行格式化,在使用 Go 的过程中通常还会需要将`New`与`fmt.Sprintf`进行组合,达到格式化的目的:
```
func E(format string, a ...interface{}) error {
return errors.New(fmt.Sprintf(format, a...))
}
```
但这种依靠字符串进行错误定义的方式的可处理性几乎为零,将会在调用上下文之间引入强依赖, 因为一个具体的错误值在`fmt`格式化封装的过程中被转移为了一个字符串类型,进而不能对 错误传播过程中错误的来源进行断言。 为此,Go 在`errors`包中引入了一系列 API 来增强错误检查的手段。
## 4.2.1 错误传播链
首先,为了建立错误传播链,`fmt.Errorf`函数允许使用`%w`动词对一个错误进行包装。 在`Errorf`的实现中,它会将需要包装的`err`包装为一个实现了`Error() string`和`Unwrap() error`两个接口的`wrapError`结构,其包含需要封装的新错误消息以及原始错误:
```
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string { return e.msg }
func (e *wrapError) Unwrap() error { return e.err }
```
`fmt`包本身对格式化的支持定义了`pp`结构,会将格式化后的内容存储在`buf`中。 但在错误传播链条的包装上,为了不破坏原始错误值,额外使用了`wrapErrs`和`wrappedErr`两个字段,其中`wrapErrs`用于格式化过程中判断是否对错误进行了包装,`wrappedErr`则用于存储原始的错误:
```
type pp struct {
buf buffer // 本质为 []byte 类型
...
wrapErrs bool
wrappedErr error // wrappedErr 记录了 %w 动词的 err
}
```
方法`Errorf`会首先使用`newPrinter`和`doPrintf`对格式进行处理, 将带有动词的格式字符串和参数进行拼接。 具体而言,`Errorf`总是假设出现`%w`动词,并`doPrintf`函数内部将对`error`类型的参数进行特殊处理。当有错误保存在`wrappedErr`时,说明需要对 错误进行一层包装,否则说明是一个原始的错误构造:
```
package fmt
import "errors"
func Errorf(format string, a ...interface{}) error {
p := newPrinter()
p.wrapErrs = true // 假设格式化过程中可能包含 %w 动词,设置为 true
p.doPrintf(format, a) // 对 format 和实际的参数进行拼接,用于后续打印
s := string(p.buf) // 拼接好的内容保存在 buf 内
var err error
if p.wrappedErr == nil {
err = errors.New(s) // 构造原始错误
} else {
err = &wrapError{s, p.wrappedErr} // 对错误进行包装
}
p.free()
return err
}
```
`doPrintf`函数最终将调用`handleMethods`方法来对错误进行记录。当遇到`%w`动词时,会判断`%w`对应的参数值是否为`error`类型,并将错误保存到`wrappedErr`内,并将后续处理退化为`%v`的后续拼接与格式化。
```
// 调用链 doPrintf -> printArg -> handleMethods
func (p *pp) handleMethods(verb rune) (handled bool) {
...
if verb == 'w' {
err, ok := p.arg.(error)
// 判断与 %w 对应的值是否为 error 类型,否则处理为错误的动词组合
if !ok || !p.wrapErrs || p.wrappedErr != nil {
...
return true
}
// 保存 err,并将其退化为 %v 动词
p.wrappedErr = err
verb = 'v'
}
...
}
```
显然,`%w`这个动词的主要目的是将`err`记录到`wrappedErr`这个同时实现了`Error() string`和`Unwrap() error`的错误中, 从而能安全的将`verb`转化为`%v`动词对参数进行后续的格式化拼接。
## 4.2.2 错误值拆包
但形成错误链条后,使用`Unwrap`便能将一个已被`fmt`包装过的`error`进行拆包, 其实现的核心思想是对错误值是否实现了`Unwrap() error`方法进行一次类型断言:
```
func Unwrap(err error) error {
// 断言 err 实现了 Unwrap 方法
u, ok := err.(interface { Unwrap() error })
if !ok { return nil }
return u.Unwrap()
}
```
在`fmt.Errorf`的实现中,已经看到,错误链条错误使用了`wrapError`进行包装, 而这一类型恰好实现了`Unwrap() error`方法。
## 4.2.3 错误断言
`Is`用于检查当前的两个错误是否相等。之所以需要这个函数是因为一个错误可能被包装了多层, 那么我们需要支持这个错误在包装过多层后的判断。 可想而知,在实现上需要一个`for`循环对其进行`Unwrap`操作:
```
func Is(err, target error) bool {
if target == nil { return err == target }
isComparable := reflect.TypeOf(target).Comparable()
for {
// 如果 target 错误是可比较的,则直接进行比较
if isComparable && err == target { return true }
// 如果 err 实现了 Is 方法,则调用其实现进行判断
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
// 否则,对 err 进行 Unwrap
if err = Unwrap(err); err == nil { return false }
// 如果 Unwrap 成功,则继续判断
}
}
```
可见`Is`方法的目的是替换使用`==`形式的错误断言:
```
if err == io.ErrUnexpectedEOF {
// ... 处理错误
}
=>
if errors.Is(err, io.ErrUnexpectedEOF) {
// ... 处理错误
}
```
值得注意的是,`Is`方法要求自定义的错误值实现`Is(error) bool`方法来进行自定义的错误断言, 否则错误的比较仍然只是使用`==`算符。
方法`As`的实现与`Is`基本类似,但不同之处在于`As`的目的是将某个错误给拆封 到具体的变量中,因此对于一个错误链而言,需要一个循环不断对错误进行`Unwrap`, 当错误值实现了`As(interface{}) bool`方法时,则可完成拆封:
```
func As(err error, target interface{}) bool {
if target == nil {
panic("errors: target cannot be nil")
}
val := reflect.ValueOf(target)
typ := val.Type()
if typ.Kind() != reflect.Ptr || val.IsNil() {
panic("errors: target must be a non-nil pointer")
}
if e := typ.Elem(); e.Kind() != reflect.Interface && !e.Implements(errorType) {
panic("errors: *target must be interface or implement error")
}
targetType := typ.Elem()
for err != nil {
// 若可直接将 err 拆封到 target
if reflect.TypeOf(err).AssignableTo(targetType) {
val.Elem().Set(reflect.ValueOf(err))
return true
}
// 判断 err 是否实现 As 方法,若已实现则直接调用
if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
return true
}
// 否则对错误链进行 Unwrap
err = Unwrap(err)
}
return false
}
var errorType = reflect.TypeOf((*error)(nil)).Elem()
```
可见,由于错误链的存在,`errors.As`方法的目的是替换类型断言式的错误断言:
```
if e, ok := err.(*os.PathError); ok {
// ... 处理错误
}
=>
var e *os.PathError
if errors.As(err, &e) {
// ... 处理错误
}
```
## 4.2.4 小结
`errors`包中对错误检查的设计通过暴露`New`、`Unwrap`、`Is`和`As`四个方法完成 在复杂函数调用链条中使用`fmt.Errorf`封装的错误传播链条的拆解。 其中`New`负责原始错误的创建,`Unwrap`允许对错误传播链条进行一次拆包,`Is`则提供了在复杂错误链中,对错误类型进行断言的能力; 而`As`解决了将错误从错误链拆解到某个目标错误类型的能力。
- 第一部分 :基础篇
- 第1章 Go语言的前世今生
- 1.2 Go语言综述
- 1.3 顺序进程通讯
- 1.4 Plan9汇编语言
- 第2章 程序生命周期
- 2.1 从go命令谈起
- 2.2 Go程序编译流程
- 2.3 Go 程序启动引导
- 2.4 主Goroutine的生与死
- 第3 章 语言核心
- 3.1 数组.切片与字符串
- 3.2 散列表
- 3.3 函数调用
- 3.4 延迟语句
- 3.5 恐慌与恢复内建函数
- 3.6 通信原语
- 3.7 接口
- 3.8 运行时类型系统
- 3.9 类型别名
- 3.10 进一步阅读的参考文献
- 第4章 错误
- 4.1 问题的演化
- 4.2 错误值检查
- 4.3 错误格式与上下文
- 4.4 错误语义
- 4.5 错误处理的未来
- 4.6 进一步阅读的参考文献
- 第5章 同步模式
- 5.1 共享内存式同步模式
- 5.2 互斥锁
- 5.3 原子操作
- 5.4 条件变量
- 5.5 同步组
- 5.6 缓存池
- 5.7 并发安全散列表
- 5.8 上下文
- 5.9 内存一致模型
- 5.10 进一步阅读的文献参考
- 第二部分 运行时篇
- 第6章 并发调度
- 6.1 随机调度的基本概念
- 6.2 工作窃取式调度
- 6.3 MPG模型与并发调度单
- 6.4 调度循环
- 6.5 线程管理
- 6.6 信号处理机制
- 6.7 执行栈管理
- 6.8 协作与抢占
- 6.9 系统监控
- 6.10 网络轮询器
- 6.11 计时器
- 6.12 非均匀访存下的调度模型
- 6.13 进一步阅读的参考文献
- 第7章 内存分配
- 7.1 设计原则
- 7.2 组件
- 7.3 初始化
- 7.4 大对象分配
- 7.5 小对象分配
- 7.6 微对象分配
- 7.7 页分配器
- 7.8 内存统计
- 第8章 垃圾回收
- 8.1 垃圾回收的基本想法
- 8.2 写屏幕技术
- 8.3 调步模型与强弱触发边界
- 8.4 扫描标记与标记辅助
- 8.5 免清扫式位图技术
- 8.6 前进保障与终止检测
- 8.7 安全点分析
- 8.8 分代假设与代际回收
- 8.9 请求假设与实务制导回收
- 8.10 终结器
- 8.11 过去,现在与未来
- 8.12 垃圾回收统一理论
- 8.13 进一步阅读的参考文献
- 第三部分 工具链篇
- 第9章 代码分析
- 9.1 死锁检测
- 9.2 竞争检测
- 9.3 性能追踪
- 9.4 代码测试
- 9.5 基准测试
- 9.6 运行时统计量
- 9.7 语言服务协议
- 第10章 依赖管理
- 10.1 依赖管理的难点
- 10.2 语义化版本管理
- 10.3 最小版本选择算法
- 10.4 Vgo 与dep之争
- 第12章 泛型
- 12.1 泛型设计的演进
- 12.2 基于合约的泛型
- 12.3 类型检查技术
- 12.4 泛型的未来
- 12.5 进一步阅读的的参考文献
- 第13章 编译技术
- 13.1 词法与文法
- 13.2 中间表示
- 13.3 优化器
- 13.4 指针检查器
- 13.5 逃逸分析
- 13.6 自举
- 13.7 链接器
- 13.8 汇编器
- 13.9 调用规约
- 13.10 cgo与系统调用
- 结束语: Go去向何方?