# 4.4 错误语义
我们其实已经在前面的讨论中详细讨论过了错误值检查与错误上下文的增强手段, 处理方式啰嗦而冗长,减少这种代码出现的密集程度真的是一个实际的问题吗?换句话说: 社区里怨声载道的冗长的错误处理语义,真的有必要进行改进吗?
### 4.4.1 check/handle 关键字
Go 团队在重新考虑错误处理的时候提出过两种不同的方案, 由 Russ Cox 提出的第一种方案就是引入新的关键字`check`/`handle` 进行组合。
我们来看这样一个复制文件的例子。复制文件操作涉及到源文件的打开、目标文件的创建、 内容的复制、源文件和目标文件的关闭。这之间任何一个环节出错,都需要错误进行处理:
```
func CopyFile(src, dst string) error {
r, err := os.Open(src)
if err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
defer r.Close()
w, err := os.Create(dst)
if err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
if _, err := io.Copy(w, r); err != nil {
w.Close()
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
if err := w.Close(); err != nil {
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
}
```
在使用`check`/`handle`组合后,我们可以将前面的代码进行化简,较少`if err != nil`的出现频率,并统一在`handle` 代码块中对错误进行处理:
```
func CopyFile(src, dst string) error {
handle err {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
r := check os.Open(src)
defer r.Close()
w := check os.Create(dst)
handle err {
w.Close()
os.Remove(dst) // (only if a check fails)
}
check io.Copy(w, r)
check w.Close() // 此处发生 err 调用上方的 handle 块时还会再额外调用一次 w.Close()
return nil
}
```
这种使用`check`和`handle`的方式会当`err`发生时,直接进入`check`关键字上方 最近的一个`handle err`块进行错误处理。在官方的这个例子中其实就已经发生了语言上模棱两可的地方, 当函数最下方的`w.Close`产生调用时, 上方与其最近的一个`handle err`还会再一次调用`w.Close`,这其实是多余的。
此外,这种方式看似对代码进行了简化,但仔细一看这种方式与`defer`函数进行错误处理之间, 除了减少了`if err != nil { return err }`出现的频率,并没有带来任何本质区别。 例如,我们完全可以使用`defer`来实现`handle`的功能:
```
func CopyFile(src, dst string) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("copy %s %s: %v", src, dst, err)
}
}()
r, err := os.Open(src)
if err != nil { return }
defer r.Close()
w, err := os.Create(dst)
if err != nil { return }
defer func() {
if err != nil {
w.Close()
os.Remove(dst)
}
}()
_, err = io.Copy(w, r)
if err != nil { return }
err = w.Close()
if err != nil { return }
}
```
在仔细衡量后不难看出,`check`/`handle`关键字的设计中,`handle`仅仅只是对现有的语义的一个化简。 具体来说,`handle`关键字等价于`defer`:
```
handle err { ... }
=>
defer func() {
if err != nil {
err = ...
}
}()
```
而`check`关键字则等价于:
```
check F()
=>
err = F()
if err != nil {
return
}
```
那么能不能仅实现一个`check`关键字呢?
### 4.4.2 内建函数`try()`
紧随`check/handle`的提案,Robert Griesemer 提出了使用内建函数`try()`配合延迟语句来替代`check`,它能够接收最后一个返回值为`error`的函数, 并将除`error`之外的返回值进行返回,即:
```
x1, x2, ..., xn = try(F())
=>
t1, ..., tn, te := F()
if te != nil {
err = te
return
}
x1, ..., xn = t1, ..., tn
```
有了`try()`函数后,可以将复制文件例子中的代码化简为:
```
func CopyFile(src, dst string) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("copy %s %s: %v", src, dst, err)
}
}()
r := try(os.Open(src))
defer r.Close()
w := try(os.Create(dst))
defer func() {
w.Close()
if err != nil {
os.Remove(dst) // 仅当 try 失败时才调用
}
}()
try(io.Copy(w, r))
try(w.Close())
return nil
}
```
可见,这种做法与`check/handle`的关键字组合本质上也没有代码更多思想上的变化, 尤其是`try()`内建函数仅仅在在形式上对`if err != nil { ... }`起到了化简的作用。
但这一错误处理语义并没有在最后被纳入语言规范。 这一设计被拒绝的核心原因是`try()`函数将使对错误的调试变得不够透明, 其本质在于将一个显式返回的错误值进行隐藏。例如,在调试过程中由于被调试函数被包裹在`try()`内,这种不包含错误分支的代码形式,对追踪错误本身是一个毁灭性的打击,为此用户不得不在调试时 引入错误分支,在调试结束后将错误分支消除,烦琐不堪。
我们从这前后两份提案中,可以看到 Go 团队将错误处理语义上的改进与 『如何减少`if err != nil { ... }`的出现』直接化了等号,这种纯粹写法风格上的问题, 与 Go 语言早期设计中显式错误值的设计相比,就显得相形见绌了。
- 第一部分 :基础篇
- 第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去向何方?