【译文】原文地址
摘要
对于来自其他面向对象语言比如C#(Go并不是你所认知典型面向对象语言)的读者来说,错误和异常几乎等同的概念。在其他语言中,因为你不确定函数是否会抛异常,因此总是使用try...catch来封装函数。取而代之的是,Go函数支持多返回值,这种能力通常用于将错误和函数结果一起返回。
func add(a,b int) (int, error){
如果你的函数会因为某些原因执行失败,你应该将提前定义的error类型返回。该函数的调用者就知道在使用返回结果前对错误进行检查。如果错误不是nil,就有责任对错误进行处理(打日志、返回、触发重试/清理等处理)。
result,err := add(a,b)
if err != nil {
// handle error
}
// continue
这是Go错误工作方式的特点。错误处理作为代码主要的一部分,并不是放在一个单独的异常模块中来处理的。某些情况下,在写代码时错误处理无法忽视。
底层逻辑
实际上error是一个内建interface类型,全局可用。实现其函数Error就可以返回一个错误消息。
type error interface {
Error() string
}
因为interface类型变量的零值是nil,任何实现error接口类型都可作为error值。我们来通过实现error接口来创建自定义错误类型。
上面的例子,我们创建了CustomError结构体类型,并实现了Error方法。该方法返回一个字符串。因此CustomError结构体实现了error接口。这样,err就是一个error类型的实现了。如果值是error类型,Println函数能自动调用其Error方法,这样就会打印Error!。
现在你可能要问就打印这么简单消息,为什么要定义错误类型这么麻烦。以上代码只是仅仅详述了其中的原理。Go提供了内置的errors包,支持可导出的New函数。该函数需要传入字符串,并返回一个error类型值。
通过上面的例子,可以看到err是*errors.errorString类型,是一个指向errors.errorString类型的指针。当使用%#v读取值,myErr是一个指向包含一个属性值的结构体指针。
因此New函数返回的是一个指向errorString结构体的指针,并包含传入的错误信息字符串。errors包中的errorString结构体实现了error接口。
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
New函数创建并返回指向errorString结构体的指针。
func New(text string) error {
return &errorString{text}
}
不同的错误处理方法
有些错误是可以通过重试来解决的-网络故障、IO操作等。
以下链接中的包实现了重试解决错误. https://godoc.org/github.com/cenkalti/backoff#example-Retry
ExponentialBackOff指数级增加重试时间。
添加上下文信息到错误中
在实际开发中,你碰到的错误不仅仅是一个简单的字符串。它将需要更多的上下文信息,例如错误发生在哪里。基于我们前面自定义的错误,来定义一个HttpError结构体,包含status和methon属性值。然后实现Error方法,这样就实现了error接口了。因此,Error方法通过使用status和method值返回更多有用信息。
我们来一点点地过下上面的例子。首先我们创建了HttpError结构体,包含status和method属性。通过实现Error方法,就实现了error接口。因此,Error方法返回详细信息包含status和method值。
GetServerResponse是一个用于模拟发送http请求并返回的模拟函数。在该例子当中,返回值为空和一个错误。这里错误是一个指向HttpError的指针,包含401状态值和GET方法。
在main函数中,我们调用GetServerResponse函数会返回字符串和错误err。然后通过类型断言得到errval值是*HttpError。因此errval包含所有错误信息。
封装错误
如果存在调用链,使用当前errors包是不可能将错误信息传递到main函数中的,因此错误的上下文信息可能会丢失。这就是github.com/pkg/errors 的用武之地了。这个包和errors包兼容的但包含一些很酷的特性。
使用github.com/pkg/errors包,还包含更多的有用函数-errors.Unwrap和errors.Is。
日志策略
golang的默认日志包并没有提供日志级别功能。这也是Logrus的优势所在。Logrus还提供了构造日志输出的功能—这是一个非常方便的功能,因为它为开发人员提供了向错误日志消息添加上下文的能力。
栈跟踪
另一个使用日志的重要方面是栈跟踪。如果你使用github.com/pkg/errors包的话,你可以这么用:
logrus.Error("Error occurred", fmt.Sprintf("%+v", err))
是否要panic
简而言之,panic将导致应用程序崩溃。当一些意想不到的问题发生时,可以使用panic。大多数情况下,在出现任何中断应用程序正常运行的问题时,都会使用panic使应用程序失败。数据库事务我们可以认为一个完美的例子。通常,应用程序在初始化时会尝试与数据库建立连接。但是如果应用程序无法与数据库建立连接,应用程序将无法继续正常运行。所以在这种情况下,应用程序应该panic。
panic将导致堆栈跟踪,这将允许我们跟踪错误。
什么时候不要panic
考虑一个允许用户登录的应用程序。如果用户尝试使用数据库中不存在的帐户登录怎么办。在这种情况下,我们不能panic。我们必须优雅地处理这个错误。我们可以用用户输入的登录详细信息记录错误,并将错误响应返回给用户。