你知道有什么比改进错误处理的语法更好吗?那就是根本不需要处理错误。
注意:
我不是服务器托管网说“删除你的错误处理”。我的建议是,修改你的代码,这样就不用处理错误了。
本节从 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
的操作来消除错误处理。
注意:
bufio.Scanner
可以扫描任何模式,但默认情况下它会查找换行符。
如果扫描程序匹配了一行文本并且没有遇到错误,则 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 %srn", st.Code, st.Reason)
if err != nil {
return err
}
for _, h := range headers {
_, err := fmt.Fprintf(w, "%s: %srn", h.Key, h.Value)
if err != nil {
return err
}
}
if _, err := fmt.Fprint(w, "rn"); err != nil {
return err
}
_, err = io.Copy(w, body)
return err
}
首先,我们使用 fmt.Fprintf
构造状态码并检查错误。 然后对于每个标题,我们写入键值对,每次都检查错误。 最后,我们使用额外的 rn
终止标题部分,检查错误之后将响应主体复制到客户端。 最后,虽然我们不需要检查 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 %srn", st.Code, st.Reason)
for _, h := range headers {
fmt.Fprintf(ew, "%s: %srn", h.Key, h.Value)
}
fmt.Fprint(ew, "rn")
io.Copy(ew, body)
return ew.err
}
将 errWriter
应用于 WriteResponse
可以显着提高代码的清晰度。 每个操作不再需要自己做错误检查。 通过检查 ew.err
字段,将错误报告移动到函数末尾,从而避免转换从 io.Copy
的两个返回值。
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net