错误处理

简单的错误处理是使用 Fprintf 和 %v 在标准错误流上输出一条消息,%v 可以使用默认格式显示任意类型的值。
为了保持示例代码简短,有时会对错误处理有意进行一定程度的忽略。明显的错误还是要处理的。但是有些出现概率很小的错误,就忽略了,不过要标记所跳过的错误检查,就是加上注释。

根据情形,将有许多可能的处理场景,接下来是5个例子。

一、将错误传递下去

最常见的情形是将错误传递下去,使得在子例程中发生的错误变为主调例程的错误。
一种是不做任何操作立即向调用者返回错误:

resp, err := http.Get(url)
if err != nil {
    return nil, err
}

还有一种,不会直接返回,因为错误信息中缺失一些关键信息:

doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
    return nil, fmt.Errorf("parsing %s as HTML: %v\n", url, err)
}

这里格式化了一条错误消息并且返回一个新的错误值??梢晕嫉拇砦笙⒉欢系靥砑由舷挛男畔⒗唇⒁桓隹啥恋拇砦竺枋?。当错误最终被程序的 main 函数处理时,它应该能够提供一个从最根本问题到总体故障的清晰因果链、这里有一个 NASA 的事故调查的例子:

genesis: crashed: no parachute: G-switch failed: bad relay orientation

因为错误频繁地串联起来,所以消息字符串首字母不应该大写而且应该避免换行。错误结果可能会很长,但能能够使用 grep 这样的工具找到需要的信息。

需要添加的关键信息
有时候可以不用添加信息直接返回,有时候需要添加一些关键信息,因为错误信息里没有。比如 os.Open 打开文件时,返回的错误不仅仅包括错误的信息,还包含文件的名字,因此调用者构造错误消息的时候不需要包含文件的名字这类信息。具体哪些信息是缺少的关键信息需要在原始的错误消息的基础上添加?
一般地,f(x) 调用只负责报告函数的行为 f 和参数值 x,因为它们和错误的上下文相关。调用者则负责添加进一步的信息,但是 f(x) 本身并不会,并且在函数内部也没有这些信息。
比如上面的 html.Parse 返回的错误信息里不可能有 url 的信息,但是,是关键信息需要添加。而 os.Open 中,文件名字也是关键信息,但是这个正是函数的参数值,所以函数本身会返回这个信息,不需要另外添加。

二、尝试重试

对于不固定或者不可预测的错误,在短暂的间隔后对操作进行重试是合乎情理的。超出一定的重试次数和限定的时间后再报错退出。
下面给出了完整的代码,暂时只看 WaitForServer 函数:

package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
    "time"
)

// 尝试连接 url 对应的服务器
// 在一分钟内使用指数退避策略进行重试
// 所有的尝试失败后返回错误
func WaitForServer(url string) error {
    const timeout = 1 * time.Minute
    deadline := time.Now().Add(timeout)
    for tries := 0; time.Now().Before(deadline); tries++ {
        _, err := http.Head(url)
        if err == nil {
            return nil // 成功
        }
        log.Printf("server not responding (%s); retrying...", err)
        time.Sleep(time.Second << uint(tries)) // 指数退避策略
    }
    return fmt.Errorf("server %s failed to respond after %s", url, timeout)
}

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "需要提供 url 参数\n")
        os.Exit(1)
    }
    url := os.Args[1]
    if err := WaitForServer(url); err != nil {
        fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
        os.Exit(1)
    }
}

这里的指数退避策略,以及尝试多次简单的超时退出的实现也很有意思。

三、输出日志并退出

接着看上面的代码,如果多次重试后依然不能成功,调用者能够输出错误然后优雅地停止程序,但一般这样的处理应该留给主程序部分:

if err := WaitForServer(url); err != nil {
    fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
    os.Exit(1)
}

通常,如果是库函数,应该将错误传递给调用者,除非这个错误表示一个内部的一致性错误,这意味着库内部存在 bug。
这里还有一个更加方便的方法是通过调用 log.Fatalf 实现上面相同的效果。和所有的日志函数一样,它默认会将时间和日期作为前缀添加到错误消息前:

if err := WaitForServer(url); err != nil {
    log.Fatalf("Site is down: %v\n", err)
}

这种带日期时间的默认格式有助于长期运行的服务器,而对于交互式的命令行工具则意义不大。
还可以自定义命令的名称作为 log 包的前缀,并且将日期和时间略去:

log.SetPrefix("wait: ")
log.SetFlags(0)

四、记录log日志

在一些错误情况下,只记录下错误信息然后程序继续运行。同样地,可以选择使用 log 包来增加日志的常用前缀:

if err := Ping(): err != nil {
    log.Printf("Ping failed: %v; networking disabled", err)
}

所有 log 函数都会为缺少换行符的日志补充一个换行符。
或者是,直接输出到标准错误流:

if err := Ping(): err != nil {
    fmt.Fprintf(os.Stderr, "Ping failed: %v; networking disabled\n", err)
}

没有用 log 函数,所以没有时间日期,当然也不需要。上面说了,对于交互式的命令工具意义不大。

五、忽略错误

在某些罕见的情况下,还可以直接安全地忽略掉整个日志:

dir, err := ioutil.TempDir("", "scratch")
if err != nil {
    return fmt.Errorf("failed to create temp dir: %v", err)
}
// 使用临时的目录
os.RemoveAll(dir)  // 忽略错误,$TMPDIR 会被周期性删除

调用 os.RemoveAll 可能会失败,但程序忽略了这个错误,原因是操作系统会周期性地清理临时目录。在这个例子中,有意的抛弃了错误,但程序的逻辑看上去就和忘记去处理一样了。要习惯考虑到每一个函数调用可能发生的出错情况,当有意忽略一个错误的时候,要清楚地注释一下你的意图。

error 接口

之前已经使用过 error 类型了,实际上它是一个接口类型,包含一个返回错误消息的方法:

type error interface {
    Error() string
}

errors 包

构造 error 最简单的方法是调用 errors.New,它会返回一个包含指定错误消息的新 error 实例。
完整的 errors 包其实只有如下的4行代码:

package errors

func New(text string) error { return &errorString{text} }

type errorString struct { s string }

func (e *errorString) Error() string { return e.s }

底层的 errorString 类型是一个结构体,而不是像其他包里那样定义字符串的别名类型。这主要是为了?;に硎镜拇砦笾滴抟饧涞模ɑ蛘咭部赡苁枪室獾模└?。
定义的 Error 方法是指针方法,而不是值方法。这样每次 New 分配的 error 实例都互不相等,即使是同样的错误值,也是不同的地址:

fmt.Println(errors.New("TEST") == errors.New("TEST")) // false

这样可以避免比如像 io.EOF 这样重要的错误,与仅仅只是包含同样错误消息的一个错误相等。

fmt.Errorf

直接调用 errors.New 的情况比较少,只在直接能取得错误值的字符串信息的时候使用:

func startCPUProfile(w io.Writer) error {
    if w == nil {
        return errors.New("nil File")
    }
    return pprof.StartCPUProfile(w)
}

更多的情况是会得到一个错误值 err,而我们可以在这个错误值之上做一点包装,还需要做字符串格式化。有一个更易用的封装函数 fmt.Errorf,它额外还提供了字符串格式化的功能,所以一般都是用这个:

doc, err := html.Parse(resp.Body)
if err != nil {
    return fmt.Errorf("parseing %s as HTML: %v", url, err)
}