Go2旨在减少错误处理的开销,但是您知道什么比改进的错误处理语法更好吗?
完全不需要处理错误。 我不是说“删除您的错误处理代码”;而是建议您更改代码,以使您没有太多要处理的错误。
本文从John Ousterhout的“
软件设计哲学 ”
一书的“定义存在的错误”一章中获得了启发。 我将尝试将他的建议应用于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 }
我们创建bufio.Reader,然后坐在循环中,调用ReadString方法,增加计数器,直到到达文件末尾,然后返回读取的行数。 这是我们要编写的代码,但是CountLines会因错误处理而变得复杂。
例如,有一个奇怪的构造:
_, err = br.ReadString('\n') lines++ if err != nil { break }
在检查错误之前,我们增加了行数-这看起来很奇怪。 之所以要这样写,是因为ReadString在按换行符之前遇到文件结尾io.EOF时将返回错误。 如果没有换行符,也会发生这种情况。
为了解决此问题,我们将重新组织逻辑以增加行数,然后查看是否需要退出循环(此逻辑仍然不正确,可以找到错误吗?)。
但是我们还没有完成检查错误。 ReadString到达文件末尾时将返回io.EOF。 这是预料之中的,ReadString需要某种方式来表示停止,没有更多要读取的内容。 因此,在将错误返回给CountLine的调用者之前,我们需要检查是否存在io.EOF错误,在这种情况下,应将其返回给调用者,否则,当一切都正常时,我们将返回nil。 这就是为什么函数的最后一行不容易的原因
return lines, err
我认为这是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的工作(bufio.Scanner可以扫描任何模板,默认情况下它会搜索新行)。
如果扫描仪发现一行文本并且未发现错误,则sc.Scan()方法将返回true。 因此,仅当扫描程序缓冲区中有一行文本时,才会调用for循环的主体。 这意味着当没有尾随换行符时,我们的重做CountLines可以正确处理情况。 现在也可以正确处理文件为空的情况。
其次,由于sc.Scan在发生错误时返回false,因此我们的for循环将在到达文件末尾或发生错误时结束。 键入bufio.Scanner会记住检测到的第一个错误,在使用sc.Err()方法退出循环后,我们将修复此错误。
最后,buffo.Scanner负责处理io.EOF,如果到达文件末尾且没有错误,则将其转换为nil。
第二个例子
我的第二个示例受Rob Pikes的“
Errors are values”博客文章的启发。
在打开,写入和关闭文件时,错误处理虽然不是很令人印象深刻,但是可以在诸如ioutil.ReadFile和ioutil.WriteFile之类的帮助程序中完成操作。 但是,在使用低级网络协议时,通常有必要直接使用I / O原语构造响应,因此错误处理可能会开始重复。 考虑一下创建HTTP / 1.1响应的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期望的单个返回值。
这不仅是很多重复的工作,每个操作(实质上是将字节写入io.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 }
errWriter履行io.Writer合同,因此可以用来迁移现有的io.Writer。 errWriter会将记录传输到其基础记录器,直到检测到错误为止。 从现在开始,它将丢弃所有条目并返回上一个错误。
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值的令人讨厌的转换。
结论
当您遇到过多的错误处理时,请尝试提取一些操作作为辅助包装器类型。
关于作者
本文的作者
Dave Cheney是Go的许多流行软件包的作者,例如
github.com/pkg/errors和
github.com/davecheney/httpstat 。