golang 错误处理原则与策略

错误处理是包的 API 设计或者应用程序用户接口的重要部分,发生错误只是许多预料行为中的一种而已。这就是 Go 语言处理错误的方法。有一些函数总是成功返回的。比如:strings.Contains 和 strconv.FormatBool 对所有可能的参数变量都有定义好的返回结果,不会调用失败——尽管还有灾难性的和不可预知的场景,像内存耗尽,这类错误的表现和起因相差甚远而且恢复的希望也很渺茫。
type error interface {
   Error() string
}
从上面的源码中可以看出,错误在 go 语言中是一个接口,甚至我们可以说它是一个普通的借口,这个普通意思是,我们不要把它与异常联系起来。与许多其他语言不同,go 语言通过使用普通的值而非异常来报告错误。比如我们常常会用到的一个方法 errors.New("错误信息"),我们也可以再看看 go 底层是如何对其包装的:
package errors

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
   return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
   s string
}

func (e *errorString) Error() string {
   return e.s
}
所以,我们要把错误看成一个普通的接口,像包装普通接口一样去包装错误接口,就不会阻碍我们对错误的理解了,然后我们再来看看错误的处理策略,遵循这些策略可以让我们设计出更加优雅的代码。

 

错误放在最后一个返回


如果当函数调发生错误时返回一个附加的结果作为错误值,习惯上将错误值作为最后一个结果返回,如下是随便在 redis 客服端找到了一个方法:
func (c *conn) Receive() (interface{}, error) {
   return c.ReceiveWithTimeout(c.readTimeout)
}
 

布尔类型返回


如果错误只有一种情况,结果通常设置为布尔类型,就像下面这个查询缓存值的例子里面,往往都返回成功,只有不存在对应的键值的时候返回错误:
value, ok := cache.Lookup(key)
if !ok {
   // ...cache[key] 不存在
}
 

对错误进行必要的包装


doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
    return nil, fmt.Errorf("parsing %s as HTML: %v", url, err)
}
fmt.Errorf 使用 fmt.Sprintf 函数格式化一条错误消息并且返回一个新的错误值。我们可以为原始的错误消息不断地添加额外的上下文信息来建立一个可读的错误描述。当错误最终被程序的 main 函数处理时,它应当能够提供一个从最根本问题到总体故障的清晰因果链。
nofound: sql: no rows in result set
 

错误重试


对于不固定或者不可预测的错误,在短时间后操作进行重试是合乎情理的,超过一定的重试次数和限定的时间后再报错退出。下面这个处理,下面是个很好的例子,限定一分钟内按指数的退避策略,它比线性的退避策略更优,也更符合真实场景。time.Second << uint(tries) 即 2 的 tries 次方,每次休眠的时间一次比上一次更久,直到退出。
package main

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

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, "usage: wait 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)
   }
}
 

优雅的停止程序


我们可以方便的调用 log.Fatalf 记录日志,并停止服务
if err := WaitForServer(url); err != nil {
    log.Fatalf("Site is down: %v\n", err)
}

日志输出如下
2006/01/02 15:04:05 Site is down: no such domain: www.hrefs.cn
 

仅仅记录错误


在有一些错误情况下,只记录下错误信息然后程序继续运行。如下记录日志,系统 log 包可以,这篇文章仅仅说错误的话题,更好的日志记录,你也可以参看这篇文章:https://www.hrefs.cn/article/golog-and-juju-errors-quickstart
 if err := Ping(); err != nil {
    log.Printf("ping failed: %v; networking disabled", err)
}

在大型项目中,我们有更好的记录错误日志的方式,不一定在每个地方都记录,而是将日志进行包装或者跟踪错误,然后将错误传递给调用者,同样也可以参考上面的文章链接
 

忽略错误


在某些罕见的情况下,我们可以直接安全的忽略掉整个错误,比如删除一个临时目录
os.RemoveAll(dir)  // 忽略错误,dir是临时目录

是否删除成功并不影响主程序,而且操作系统会周期性的清理临时目录
Posted by 何敏 on 2020-02-18 07:41:20