你知道有什么比改进错误处理的语法更好吗?那就是根本不需要处理错误。
本节从 John Ousterhout 最近的著作“软件设计哲学”[9]中汲取灵感。该书的其中一章是“定义不存在的错误”。我们将尝试将此建议应用于 Go 语言。
计算行数
让我们编写一个函数来计算文件中的行数。
func CountLines(r io.Reader) (int, error) {
var (
br = bufio.NewReader(r)
lines int
err error
)
for {
_, err = br.ReadString('\n')
lines++
if err != nil {
break
}
}
if err != io.EOF {
return 0, err
}
return lines, nil
}
由于我们遵循前面部分的建议,CountLines
需要一个 io.Reader
,而不是一个 *File
;它的任务是调用者为我们想要计算的内容提供 io.Reader
。
我们构造一个 bufio.Reader
,然后在一个循环中调用 ReadString
方法,递增计数器直到我们到达文件的末尾,然后我们返回读取的行数。
至少这是我们想要编写的代码,但是这个函数由于需要错误处理而变得更加复杂。 例如,有这样一个奇怪的结构:
_, err = br.ReadString('\n')
lines++
if err != nil {
break
}
我们在检查错误之前增加了行数,这样做看起来很奇怪。
我们必须以这种方式编写它的原因是,如果在遇到换行符之前就读到文件结束,则 ReadString
将返回错误。如果文件中没有换行符,同样会出现这种情况。
为了解决这个问题,我们重新排列逻辑增来加行数,然后查看是否需要退出循环。
但是我们还没有完成检查错误。当 ReadString
到达文件末尾时,预期它会返回 io.EOF
。ReadString
需要某种方式在没有什么可读时来停止。因此,在我们将错误返回给 CountLine
的调用者之前,我们需要检查错误是否是 io.EOF
,如果不是将其错误返回,否则我们返回 nil
说一切正常。
我认为这是 Russ Cox 观察到错误处理可能会模糊函数操作的一个很好的例子。我们来看一个改进的版本。
func CountLines(r io.Reader) (int, error) {
sc := bufio.NewScanner(r)
lines := 0
for sc.Scan() {
lines++
}
return lines, sc.Err()
}
这个改进的版本从 bufio.Reader
切换到 bufio.Scanner
。
在 bufio.Scanner
内部使用 bufio.Reader
,但它添加了一个很好的抽象层,它有助于通过隐藏 CountLines
的操作来消除错误处理。
如果扫描程序匹配了一行文本并且没有遇到错误,则 sc.Scan()
方法返回 true
。因此,只有当扫描仪的缓冲区中有一行文本时,才会调用 for
循环的主体。这意味着我们修改后的 CountLines
正确处理没有换行符的情况,并且还处理文件为空的情况。
其次,当 sc.Scan
在遇到错误时返回 false
,我们的 for
循环将在到达文件结尾或遇到错误时退出。bufio.Scanner
类型会记住遇到的第一个错误,一旦我们使用 sc.Err()
方法退出循环,我们就可以获取该错误。
最后, sc.Err()
负责处理 io.EOF
并在达到文件末尾时将其转换为 nil
,而不会遇到其他错误。
WriteResponse
我的第二个例子受到了 Errors are values
博客文章[10]的启发。
在本章前面我们已经看过处理打开、写入和关闭文件的示例。错误处理是存在的,但是接收范围内的,因为操作可以封装在诸如 ioutil.ReadFile
和 ioutil.WriteFile
之类的辅助程序中。但是,在处理底层网络协议时,有必要使用 I/O
原始的错误处理来直接构建响应,这样就可能会变得重复。看一下构建 HTTP
响应的 HTTP
服务器的这个片段。
type Header struct {
Key, Value string
}
type Status struct {
Code int
Reason string
}
func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
_, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
if err != nil {
return err
}
for _, h := range headers {
_, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value)
if err != nil {
return err
}
}
if _, err := fmt.Fprint(w, "\r\n"); err != nil {
return err
}
_, err = io.Copy(w, body)
return err
}
首先,我们使用 fmt.Fprintf
构造状态码并检查错误。 然后对于每个标题,我们写入键值对,每次都检查错误。 最后,我们使用额外的 \r\n
终止标题部分,检查错误之后将响应主体复制到客户端。 最后,虽然我们不需要检查 io.Copy
中的错误,但我们需要将 io.Copy
返回的两个返回值形式转换为 WriteResponse
的单个返回值。
这里很多重复性的工作。 我们可以通过引入一个包装器类型 errWriter
来使其更容易。
errWriter
实现 io.Writer
接口,因此可用于包装现有的 io.Writer
。 errWriter
写入传递给其底层 writer
,直到检测到错误。 从此时起,它会丢弃任何写入并返回先前的错误。
type errWriter struct {
io.Writer
err error
}
func (e *errWriter) Write(buf []byte) (int, error) {
if e.err != nil {
return 0, e.err
}
var n int
n, e.err = e.Writer.Write(buf)
return n, nil
}
func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
ew := &errWriter{Writer: w}
fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
for _, h := range headers {
fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value)
}
fmt.Fprint(ew, "\r\n")
io.Copy(ew, body)
return ew.err
}
将 errWriter
应用于 WriteResponse
可以显着提高代码的清晰度。 每个操作不再需要自己做错误检查。 通过检查 ew.err
字段,将错误报告移动到函数末尾,从而避免转换从 io.Copy
的两个返回值。