Golang有不少优势,这也是它如此流行的主要缘由。可是 Go 1 对错误处理的支持过于简单了,以致于平常开发中会有诸多不便利,遭到不少开发者的吐槽。 这些不足催生了一些开源解决方案。与此同时, Go 官方也在从语言和标准库层面做出改进。 这篇文章将给出几种常见建立错误的方式并分析一些常见问题,对比各类解决方案,并展现了迄今为止(go 1.13)的最佳实践。git
首先介绍几种常见的建立错误的方法github
err1 := errors.New("math: square root of negative number")
err2 := fmt.Errorf("math: square root of negative number %g", x)
复制代码
package zError
import (
"fmt"
"github.com/satori/go.uuid"
"log"
"runtime/debug"
"time"
)
type BaseError struct {
InnerError error
Message string
StackTrace string
Misc map[string]interface{}
}
func WrapError(err error, message string, messageArgs ...interface{}) BaseError {
return BaseError{
InnerError: err,
Message: fmt.Sprintf(message, messageArgs),
StackTrace: string(debug.Stack()),
Misc: make(map[string]interface{}),
}
}
func (err *BaseError) Error() string {
// 实现 Error 接口
return err.Message
}
复制代码
开发中常常须要检查返回的错误值并做相应处理。下面给出一个最简单的示例。golang
import (
"database/sql"
"fmt"
)
func GetSql() error {
return sql.ErrNoRows
}
func Call() error {
return GetSql()
}
func main() {
err := Call()
if err != nil {
fmt.Printf("got err, %+v\n", err)
}
}
//Outputs:
// got err, sql: no rows in result set
复制代码
有时须要根据返回的error类型做不一样处理,例如:sql
import (
"database/sql"
"fmt"
)
func GetSql() error {
return sql.ErrNoRows
}
func Call() error {
return GetSql()
}
func main() {
err := Call()
if err == sql.ErrNoRows {
fmt.Printf("data not found, %+v\n", err)
return
}
if err != nil {
// Unknown error
}
}
//Outputs:
// data not found, sql: no rows in result set
复制代码
实践中常常须要为错误增长上下文信息后再返回,以方便调用者了解错误场景。例如 Getcall 方法时常写成:ui
func Getcall() error {
return fmt.Errorf("GetSql err, %v", sql.ErrNoRows)
}
复制代码
不过这个时候 err == sql.ErrNoRows
就不成立了。除此以外,上述写法都在返回错误时都丢掉了调用栈这个重要的信息。咱们须要更灵活、更通用的方式来应对此类问题。spa
针对存在的不足,目前有几种解决方案。这些方式能够对错误进行上下文包装,并携带原始错误信息, 还能尽可能保留完整的调用栈debug
如今咱们用这三个方法来重写上面的代码:code
import (
"database/sql"
"fmt"
"github.com/pkg/errors"
)
func GetSql() error {
return errors.Wrap(sql.ErrNoRows, "GetSql failed")
}
func Call() error {
return errors.WithMessage(GetSql(), "bar failed")
}
func main() {
err := Call()
if errors.Cause(err) == sql.ErrNoRows {
fmt.Printf("data not found, %v\n", err)
fmt.Printf("%+v\n", err)
return
}
if err != nil {
// unknown error
}
}
/*Output: data not found, Call failed: GetSql failed: sql: no rows in result set sql: no rows in result set main.GetSql /usr/three/main.go:11 main.Call /usr/three/main.go:15 main.main /usr/three/main.go:19 runtime.main ... */
复制代码
从输出内容能够看到, 使用 %v 做为格式化参数,那么错误信息会保持一行, 其中依次包含调用栈的上下文文本。 使用 %+v ,则会输出完整的调用栈详情。 若是不须要增长额外上下文信息,仅附加调用栈后返回,可使用 WithStack 方法:继承
func GetSql() error {
return errors.WithStack(sql.ErrNoRows)
}
复制代码
注意:不管是 Wrap , WithMessage 仍是 WithStack ,当传入的 err 参数为 nil 时, 都会返回nil, 这意味着咱们在调用此方法以前无需做 nil 判断,保持了代码简洁接口
结合社区反馈,Go 团队完成了在 Go 2 中简化错误处理的提案。 Go核心团队成员 Russ Cox 在xerrors中部分实现了提案中的内容。它用与 github.com/pkg/errors类似的思路解决同一问题, 引入了一个新的 fmt 格式化动词: %w,使用 Is 进行判断。
import (
"database/sql"
"fmt"
"golang.org/x/xerrors"
)
func Call() error {
if err := GetSql(); err != nil {
return xerrors.Errorf("bar failed: %w", GetSql())
}
return nil
}
func GetSql() error {
return xerrors.Errorf("GetSql failed: %w", sql.ErrNoRows)
}
func main() {
err := Call()
if xerrors.Is(err, sql.ErrNoRows) {
fmt.Printf("data not found, %v\n", err)
fmt.Printf("%+v\n", err)
return
}
if err != nil {
// unknown error
}
}
/* Outputs: data not found, Call failed: GetSql failed: sql: no rows in result set bar failed: main.Call /usr/four/main.go:12 - GetSql failed: main.GetSql /usr/four/main.go:18 - sql: no rows in result set */
复制代码
与 github.com/pkg/errors 相比,它有几点不足:
Go 1.13 将 xerrors 的部分功能(不是所有)整合进了标准库。 它继承了上面提到的 xerrors 的所有缺点, 并额外贡献了一项。所以目前没有使用它的必要。
import (
"database/sql"
"errors"
"fmt"
)
func Call() error {
if err := GetSql(); err != nil {
return fmt.Errorf("Call failed: %w", GetSql())
}
return nil
}
func GetSql() error {
return fmt.Errorf("GetSql failed: %w", sql.ErrNoRows)
}
func main() {
err := Call()
if errors.Is(err, sql.ErrNoRows) {
fmt.Printf("data not found, %+v\n", err)
return
}
if err != nil {
// unknown error
}
}
/* Outputs: data not found, Call failed: GetSql failed: sql: no rows in result set */
复制代码
上面的代码与 xerrors 版本很是接近。可是它不支持调用栈信息输出, 根据官方的说法, 此功能没有明确的支持时间。所以其实用性远低于 github.com/pkg/errors。
经过以上对比, 相信你已经有了选择。 再明确一下个人见解,若是你正在使用 github.com/pkg/errors
,那就保持现状吧。目前尚未比它更好的选择。若是你已经大量使用 golang.org/x/xerrors
, 别盲目换成 go 1.13 的内置方案。 总的来讲,Go 在诞生之初就在各个方面表现得至关成熟、稳健。 在演进路线上不多出现犹疑和摇摆, 而在错误处理方面倒是个例外。 除了被普遍吐槽的 if err != nil 以外, 就连其改进路线也备受争议、分歧明显,以至于一个改进提案都会由于压倒性的反对意见而不得不做出调整。 好在 Go 团队比之前更加乐于倾听社区意见,团队甚至专门就此问题建了个反馈收集页面。相信最终你们会找到更好的解决方案。