golang错误处理

1. 错误

1.1 error类型

错误用内建的error类型来表示(go/src/builtin/builtin.go)。

type error interface {  
    Error() string
}

error 有了一个签名为 Error() string 的方法。所有实现该接口的类型都可以当作一个错误类型。Error()方法给出了错误的描述。

package main
import (
        "fmt"
        "os"
)

func main(){
        f, err := os.Open("/test.txt")
        if err != nil {
                fmt.Println(err)
                return
        }
        fmt.Println(f.Name(), "opened successfully")
}
output:
open /test.txt: no such file or directory

fmt.Println 在打印错误时,会在内部调用 Error() string 方法来得到该错误的描述。

1.2 error操作

 error相关变量和方法定义在包errors中(go/src/errors),不同版本go中操作方法有增减,go1.17.2中主要包含errors.go和wrap.go两个文件。errors.go定义了errors.New(text string) error方法,wrap.go定义了errors.Unwrap(err error) error,errors.Is(err, target error) bool和errors.As(err error, target interface{}) bool方法。

创建wrapped errors的简单方法是调用fmt.Errorf(对参数应用%w选项)。

errors.Is reports whether any error in err's chain matches target. (if errors.Is(err, fs.ErrExist)优于直接==比较(if err == fs.ErrExist)。
errors.As finds the first error in err's chain that matches target, and if so, sets target to that error value and returns true. Otherwise, it returns false. 第二个参数target必须是指针。
//    var perr *fs.PathError
//    if errors.As(err, &perr) {
//        fmt.Println(perr.Path)
//    }
//
// is preferable to
//
//    if perr, ok := err.(*fs.PathError); ok {
//        fmt.Println(perr.Path)
//    }
err1 := fmt.Errorf("file error: %w", io.EOF)
fmt.Println(err1) // file error: EOF
err2 := fmt.Errorf("service error: %w", err1)
fmt.Println(err2) // service error: file error: EOF
err0 := errors.Unwrap(err2)
fmt.Printf("%+v\n", err0) // file error: EOF

if errors.Is(err2, io.EOF) {
    fmt.Printf("error [%+v] find io.EOF", err2)
}

常见error定义:

var EOF = errors.New("EOF")    // io

常用的错误处理方式:

0)err != nil

1) 预定义error,也称为sentinel error哨兵error,直接比较即可。

filepath 包中的 ErrBadPattern 定义如下:

var ErrBadPattern = errors.New("syntax error in pattern")

直接比较错误类型

    files, error := filepath.Glob("[")
    if error != nil && error == filepath.ErrBadPattern {
        fmt.Println(error)
        return
    }

注:这里要特别注意的是,比较的两个error都要来自相同的包才能相等,因为sentinel error一般都是通过errors.New()生成的,errors.New()返回结构体指针,只有返回同一个error.New()的地址才相等,自己用errors.New()创建的error地址明显和调用包的函数返回的error地址不同。

注:指针地址用%p打印,%X打印的指针值。

    err := errors.New("EOF")
    if err == io.EOF {
        fmt.Println("EOF equal")
    }

    fmt.Printf("err type %T, value %v, %X, %p\n", err, err, err, err)
    fmt.Printf("io.EOF type %T, value %v, %X, %p\n", io.EOF, io.EOF, io.EOF, io.EOF)
------
err type *errors.errorString, value EOF, 454F46, 0xc000084200
io.EOF type *errors.errorString, value EOF, 454F46, 0xc000084050

2)     断言底层结构体类型,使用结构体字段获取更多信息

通过阅读Open函数文档可知,返回的错误类型是*PathError。

type PathError struct {  
    Op   string
    Path string
    Err  error
}

func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }

可通过断言*PathError类型,获取结构体详细字段内容:

err是接口变量,err(*os.PathError)断言err的类型*os.PathError,从而获取接口的底层值,即*PathError。

    f, err := os.Open("/test.txt")
    if err, ok := err.(*os.PathError); ok {
        fmt.Println("File at path", err.Path, "failed to open")
        return
    }

3)通过errors.New()或fmt.Errorf()自定义错误。此时,error就不可以与自定义的error比较,需要Is或As方法。

4)opaque错误,不透明错误。只判断error是否是nil,即是否发生错误;通过包函数判断错误行为,如IsExist(),暴露较少API和变量,推荐

上述1)2)都需要包含相应的错误包(即要事先知道error定义),不优雅。

2.自定义错误

创建自定义错误最简单的方法是使用 errors 包中的 New 函数。

package errors

// New returns an error that formats as the given text.
// 特别注意这里返回的是指针,即即使两个text string相同,返回的地址也不相同,
// 确保了字符串相等,而error不同的情况
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 }

在函数中应用errors.New():

func circleArea(radius float64) (float64, error) {  
    if radius < 0 {
        return 0, errors.New("Area calculation failed, radius is less than zero")
    }
    return math.Pi * radius * radius, nil
}

fmt.Errorf()打印错误信息

    if radius < 0 {
        return 0, fmt.Errorf("Area calculation failed, radius %0.2f is less than zero", radius)
    }

使用结构体类型和字段提供错误的更多信息

使用指针接收者 *areaError,实现了 error 接口的 Error() string 方法。

type areaError struct {  
    err    string
    radius float64
}

func (e *areaError) Error() string {  
    return fmt.Sprintf("radius %0.2f: %s", e.radius, e.err)
}

    if radius < 0 {
        return 0, &areaError{"radius is negative", radius}
    }

使用结构体类型的方法来提供错误的更多信息

type areaError struct {  
    err    string //error description
    length float64 //length which caused the error
    width  float64 //width which caused the error
}
func (e *areaError) Error() string {  
    return e.err
}

func (e *areaError) lengthNegative() bool {  
    return e.length < 0
}

func (e *areaError) widthNegative() bool {  
    return e.width < 0
}
func rectArea(length, width float64) (float64, error) {  
    err := ""
    if length < 0 {
        err += "length is less than zero"
    }
    if width < 0 {
        if err == "" {
            err = "width is less than zero"
        } else {
            err += ", width is less than zero"
        }
    }
    if err != "" {
        return 0, &areaError{err, length, width}
    }
    return length * width, nil
}

3.panic

函数发生 panic 时,它会终止运行,在执行完所有的延迟函数后,程序控制返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪(Stack Trace),最后程序终止。

需要注意的是,你应该尽可能地使用错误,而不是使用 panic 和 recover。只有当程序不能继续运行的时候,才应该使用 panic 和 recover 机制。只有当真正遇到fatal错误时才使用panic。

panic 有两个合理的用例。

  • l  发生了一个不能恢复的错误,此时程序不能继续运行。 一个例子就是 web 服务器无法绑定所要求的端口。在这种情况下,就应该使用 panic,因为如果不能绑定端口,啥也做不了。
  • l  发生了一个编程上的错误。 假如我们有一个接收指针参数的方法,而其他人使用 nil 作为参数调用了它。在这种情况下,我们可以使用 panic,因为这是一个编程错误:用 nil 参数调用了一个只能接收合法指针的方法。
func panic(interface{})

panic(errors.New("error is ..."))

recover 是一个内建函数,用于重新获得 panic 协程的控制。

func recover() interface{}

recover()返回的是panic()传入的interface,一般为error。

只有在延迟函数的内部,调用 recover 才有用。在延迟函数内调用 recover,可以取到 panic 的错误信息,并且停止 panic 续发事件(Panicking Sequence),程序运行恢复正常。如果在延迟函数的外部调用 recover,就不能停止 panic 续发事件。

只有在相同的 Go 协程中调用 recover 才管用。recover 不能恢复一个不同协程的 panic。

    //创建定时器并设置定时时间
    t1 := time.NewTicker(300 * time.Second)
    t2 := time.NewTicker(300 * time.Second)
    defer t1.Stop()
    defer t2.Stop()
    for {
        select {
        case <-t1.C:
            defer func() {
                if err := recover(); err != nil {
                    log.Println("异常信息为 : ", err) // 这里的err其实就是panic传入的内容
                }
            }()
            dosomething()
        case <-t2.C:
            defer func() { //必须要先声明defer,否则不能捕获到panic异常
                if err := recover(); err != nil {
                    log.Println("Type Of Error : ", err) // 这里的err其实就是panic传入的内容
                }
            }()
            dosomething()
        }
    }

4. 错误使用最佳实践 

使用github.com/pkg/errors,在go1.13中errors部分集成了github.com/pkg/errors方法。

常用的err!=nil不能提供上下文或调试信息。

func Cause(err error) error  // 返回底层error原因,即原始的error(以前常用的error!=nil中的error),需要err实现causer接口:

type causer interface {
       Cause() error
}

If the error does not implement Cause, the original error will be returned. If the error is nil, nil will be returned without further investigation.

调用err.Cause()获取原始的error即根因,可用于sentinel error的等值判断。

func Wrap(err error, message string) error  // 包装堆栈信息的error。

func Wrapf(err error, format string, args ...interface{}) error

若error是nil,Wrap返回nil。

errors.New()和errors.Errorf()都会返回携带堆栈信息的错误。

func New(message string) error
func Errorf(format string, args ...interface{}) error

github.com/errors返回的error values都可被格式话输出:

%s    print the error. If the error has a Cause it will be printed recursively.
%v    see %s
%+v   extended format. Each Frame of the error's StackTrace will be printed in detail.
type withMessage struct {
    cause error
    msg   string
}
func WithMessage(err error, message string) error
func WithMessagef(err error, format string, args ...interface{}) error

WithMessage()添加message信息到error中,不包含堆栈信息。

使用规则:

1)在应用代码中(自己程序中),使用errors.New()或者erros.Errorf()返回错误。

2)若果调用其他包内的函数,通常简单的直接返回。

3)若果和其他库进行协作,包括标准库,考虑使用errors.Wrap或者errors.Wrapf保存堆栈信息。

4)直接返回错误,而不是每个错误产生的地方到处打日志。

5)在程序的顶部或工作的goroutine顶部(请求入口),使用%+v把堆栈详情记录。

 

参考:Go 系列教程 —— 32. panic 和 recover

posted @ 2019-07-03 20:46  yuxi_o  阅读(432)  评论(0编辑  收藏  举报