通过消除错误消除错误处理
Go 2 旨在改善错误处理的开销, 但是你知道有什么比处理错误的改进语法更好吗? 根本不需要处理错误. 现在, 我不是说 “删除你的错误处理代码”, 相反我建议改变你的代码, 这样你就没有多少错误需要处理.
本文从 John Ousterhout 软件设计的哲学的一章中汲取灵感, “定义不存在的错误”. 我尝试将他的建议应用于 Go.
这是一个计算文件行数的函数:
func CountLines(r io.Reader) (int, error) { |
我们构造一个 bufio.Reader, 然后在一个循环中调用 ReadString 方法, 递增计数器直到我们到达文件的末尾, 然后我们返回读取的行数. 这是我们想编写的代码, 而 CountLines 因错误处理而变得更加复杂.
例如, 有这种奇怪的结构:
_, err = br.ReadString('\n') |
我们在检查错误之前增加行数, 这看起来很奇怪. 我们必须以这种方式编写它的原因是, 如果在遇到换行符之前遇到文件结尾-io.EOF, 则 ReadString 将返回错误. 如果没有尾换行符, 则会发生这种情况.
为了定位这个问题, 我们重整逻辑以增加行数, 然后查看是否需要退出循环.
但是我们还没有完成错误检查. 当 ReadString 到达文件末尾时, 它将返回 io.EOF. 这是预期的, ReadString 需要某种方式来终止, 没有更多内容需要读取. 所以在我们将错误返回给 CountLine 的调用者之前, 我们需要检查错误是不是 io.EOF, 并且在这种情况下将其传递, 否则我们返回 nil 表示一切正常. 这就是为什么函数的最后一行不是简单的:
return lines, err |
我认为这是 Russ Cox 错误的处理可能会模糊函数操作的一个很好的例子. 我们来看一个改进的版本:
func CountLines(r io.Reader) (int, error) { |
这个改进的版本从使用 bufio.Reader 切换到 bufio.Scanner. 本质上 bufio.Scanner 使用 bufio.Reader 添加一层抽象, 这有助于消除掩盖我们之前版本的 CountLines 的操作的错误处理.
如果扫描器匹配了一行文本并且没有遇到错误, 则 sc.Scan() 方法返回 true. 因此, 只有当扫描器的缓冲区中有一行文本时, 才会调用 for 循环体. 这意味着我们修改后的 CountLines 正确处理了没有尾换行符的情况, 它还正确处理了文件为空的情况.
其次, 当 sc.Scan 遇到错误时返回 false, 当到达文件结尾或遇到错误时, 我们的 for 循环将退出. bufio.Scanner 类型会记住遇到的第一个错误, 一旦我们使用 sc.Err() 方法退出循环, 我们就恢复该错误.
最后, buffo.Scanner 负责处理 io.EOF 并在到文件末尾时将其转换为 nil, 而不会遇到其他错误.
我的第二个例子的灵感来自于 Rob Pike 的错误是值的博客文章.
在处理打开, 写入和关闭文件时, 错误处理存在但不是压倒性的, 因为操作可以封装在诸如 ioutil.ReadFile 和 ioutil.WriteFile 之类的帮助程序中. 但是, 在处理低级网络协议时, 通常需要使用 I/O 原语直接构建响应, 因此错误处理可能变得重复. 考虑构建 HTTP/1.1 响应的 HTTP 服务器的这个片段:
type Header struct { |
首先, 我们使用 fmt.Fprintf 构造状态行, 并检查错误. 然后对于每个头, 我们写入键和值, 每次都检查错误. 最后, 我们使用额外的 \r\n 终止头部分, 检查错误, 并将响应体复制到客户端. 最后, 虽然我们不需要检查来自 io.Copy 的错误, 但我们需要将它从 io.Copy 返回的两个返回值形式转换为 WriteResponse 期望的单个返回值.
这不仅是大量的重复工作, 每个操作 - 从根本上将字节写入 io.Writer - 具有不同形式的错误处理. 但是我们可以通过引入一个小包装类型来变得容易:
type errWriter struct { |
errWriter 实现了 io.Writer 接口, 所以可以用它来包装现有的 io.Writer. errWriter 将写入传递给其底层写入器(writer), 直到检测到错误. 从那起, 它会丢弃任何写入并返回先前的错误.
func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error { |
将 errWriter 应用于 WriteResponse 可以显著提高代码的清晰度. 每个操作不再依附在一个错误处理块. 通过检查 ew.err 字段, 将错误报告移动到函数的末尾, 避免从 io.Copy 的返回值进行恼人的转换.
当你发现自己面临恼人的错误处理时, 请尝试将某些操作提取到帮助程序类型中.