[TOC]
# 4.1 问题的演化
错误`error`在 Go 中表现为一个内建的接口类型,任何实现了`Error() string`方法的类型都能作为`error`类型进行传递,成为错误值:
```
type error interface {
Error() string
}
```
作为内建接口类型,编译器负责在参数传递检查时,对值类型所实现的方法进行检查。 当类型实现了`Error() string`方法后,才允许其作为 error 进行传递:
```
// go/src/cmd/compile/internal/gc/universe.go
func makeErrorInterface() *types.Type {
field := types.NewField()
field.Type = types.Types[TSTRING]
f := functypefield(fakeRecvField(), nil, []*types.Field{field})
// 查找是否实现了 Error
field = types.NewField()
field.Sym = lookup("Error")
field.Type = f
t := types.New(TINTER)
t.SetInterface([]*types.Field{field})
return t
}
```
## 4.1.1 错误的历史形态
早期的 Go 甚至没有错误处理 \[Gerrand, 2010\] \[Cox, 2019b\], 当时的`os.Read`函数进行系统调用可能产生错误,而该接口是通过`int64`类型进行错误返回的:
```
export func Read(fd int64, b *[]byte) (ret int64, errno int64) {
r, e := syscall.read(fd, &b[0], int64(len(b)));
return r, e
}
```
随后,Go 团队将这一`errno`转换抽象成了一个类型:
```
export type Error struct { s string }
func (e *Error) Print() { ... }
func (e *Error) String() string { ... }
export func Read(fd int64, b *[]byte) (ret int64, err *Error) {
r, e := syscall.read(fd, &b[0], int64(len(b)));
return r, ErrnoToError(e)
}
```
之后才演变为了 Go 1 中被人们熟知的`error`接口类型。
可见之所以从理解上我们可以将 error 认为是一个接口,是因为在编译器实现中, 是通过查询某个类型是否实现了`Error`方法来创建 Error 类型的。
## 4.1.2 处理错误的基本策略
由于 Go 中的错误处理设计得非常简洁,在其他现代编程语言里都几乎找不见此类做法。 Go 团队也曾多次撰写文章来教导 Go 语言的用户 \[Gerrand, 2011\] \[Pike, 2015\]。 无论怎样,非常常见的策略包含哨兵错误、自定义错误以及隐式错误三种。
### 哨兵错误
哨兵错误的处理方式通过特定值表示成功和不同错误,依靠调用方对错误进行检查:
```
if err === ErrSomething { ... }
```
例如,比较著名的`io.EOF = errors.New("EOF")`。
这种错误处理的方式引入了上下层代码的依赖,如果被调用方的错误类型发生了变化, 则调用方也需要对代码进行修改:
```
func readf(path string) error {
err := file.Open(path)
if err != nil {
return fmt.Errorf("cannot open file: %v", err)
}
}
func main() {
err := readf("~/.ssh/id_rsa.pub")
if strings.Contains(err.Error(), "not found") {
...
}
}
```
这类错误处理的方式是非常危险的,因为它在调用方和被调用方之间建立了牢不可破的依赖关系。 除此之外,哨兵错误还有一个相当致命的危险,那就是这种方式所定义的错误并非常量,例如:
```
package io
var EOF = errors.New("EOF")
```
而当我们将此错误类型公开给其他包使用后,我们非常难以避免这种事情发生:
```
package main
import "io"
func init() {
io.EOF = nil
}
```
这种事情甚至严重到,如果在引入的依赖中,有人恶意将这样验证错误值进行修改的代码包含进去, 将导致重大的安全问题:
```
import "cropto/rsa"
func init() {
rsa.ErrVerification = nil
}
```
在硕大的代码依赖中,我们几乎无法保证这种恶意代码不会出现在某个依赖的包中。 为了安全起见,变量错误类型可以修改为常量错误:
```
-var EOF = errors.New("EOF")
+const EOF = ioError("EOF")
+type ioEorror string
+
+func (e ioError) Error() string { return string(e) }
```
### 自定义错误
```
if err, ok := err.(SomeErrorType); ok { ... }
```
这类错误处理的方式通过自定义的错误类型来表示特定的错误,同样依赖上层代码对错误值进行检查, 不同的是需要使用类型断言进行检查。 例如:
```
type CustomizedError struct {
Line int
Msg string
File string
}
func (e CustomizedError) Error() string {
return fmt.Sprintf("%s:%d: %s", e.File, e.Line, e.Msg)
}
```
这种错误处理的好处在于,可以将错误包装起来,提供更多的上下文信息, 但错误的实现方必须向上层公开实现的错误类型,不可避免的同样需要产生依赖关系。
### 隐式错误
```
if err != nil { return err }
```
这种错误处理的方式直接返回错误的任何细节,直接将错误进一步报告给上层。这种情况下, 错误在当前调用方这里完全没有进行任何加工,与没有进行处理几乎是等价的, 这会产生的一个致命问题在于:丢失调用的上下文信息,如果某个错误连续向上层传播了多次, 那么上层代码可能在输出某个错误时,根本无法判断该错误的错误信息究竟从哪儿传播而来。 以上面提到的文件打开的例子为例,错误信息可能就只有一个`not found`。
## 4.1.3 处理错误的本质
回顾处理错误的基本策略我们可以看出,在 Go 语言中错误处理这一话题基本上是围绕以下三个问题进行的:
1. 错误值检查:如何对一个传播链条中的错误类型进行断言?
2. 错误格式与上下文:出现错误时,没有足够的堆栈信息,如何增强错误发生时的上下文信息并合理格式化一个错误?
3. 错误处理语义:每个返回错误的函数都要求调用方进行显式处理,处理方式啰嗦而冗长,如何减少这种代码出现的密集程度?
我们在后面的小节中对这些问题进行一一讨论。
- 第一部分 :基础篇
- 第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去向何方?