error的处理方式

前言:
此文是观看毛剑老师的《GO进阶训练营》中的《异常处理》的个人理解和总结,如果有不对的地方,还望指正,谢谢🙏。

error和exception

exception无法区分普通异常和致命异常,始终需要使用try...catch命令去捕捉异常,在自己编码中经常会去考虑是否对其异常捕获,甚至有时会对异常catch多次。

go中的error利用多返回值,使用普通errorpanic分别代表了普通异常和致命异常,更加明确了对异常的处理。

一、什么是error

go中的error实际上是实现了error接口的普通类型,这样可以很容易实现自定义的error

// https://golang.org/src/builtin/builtin.go
type error interface {
    Error() string
}

官方标准库中的errors

// https://golang.org/src/errors/errors.go
package errors
func New(text string) error {
    return &errorString{text}
}

type errorString struct {
    s string
}

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

其中New方法返回errorString内存地址是为了避免在使用相同errors包时,不同的错误因为传入的文字信息相同而相等。

package main

import "fmt"

type myError struct {
    s string
}

func (m myError) Error() string {
    return m.s
}

func New(text string) error {
    return myError{text}
}

func main() {
    aErr := New("error")
    bErr := New("error")
    if aErr == bErr {
        fmt.Println("aErr == bErr") // aErr == bErr
    }
}

上述代码会认为分别New出来的error时相等的,这样明显是不正确的,当修改返回的error是内存地址时,即使相同文字也不会相等。

二、error和panic使用场景

error一般用于可遇见的普通异常,比如参数错误、查询错误等异常;而panic一般用于强依赖的第三方、配置信息发生错误,比如db无法连接、配置参数不存在等异常,但是使用方式需要根据实际代码逻辑进行判断。

三、error的几种使用方式

1、Sentinal Error

Sentinal Error是预先在包中定义的特定错误,比如io包中的EOF

// https://golang.org/src/io/io.go
var EOF = errors.New("EOF")

io.EOF是用于在文件读取时没有内容返回的错误,所以在外部接收到error时需要判断是否等于io.EOF

if err != nil {
    if err == io.EOF {
        // do something
    }
}

使用Sentinal Error有以下不足:

  • 无法携带多余的上下文信息,如果使用fmt.Errorf添加自定义信息,返回的错误无法再与Sentinal Error进行比较。

  • 使包与包之间产生耦合,假如存在Sentinal Error的包是项目的基础包,引入这个基础的其他包都必须导入这些错误值,增加了耦合性的同时基础包也无法再对这些错误值进行相关修改。

所以Sentinal Error尽量避免在基础包(或者很多包引用的包)中使用。

2、Error Type

Error Type是实现了error接口的自定义类型,可以保留底层的错误同时增加上下文信息。

例如myError类型记录了文件和行号的上下文:

package main

import (
    "fmt"
)

type myError struct {
    s string
    file string
    line int
}

func (m *myError) Error() string {
    return fmt.Sprintf("%s-%d: %s", m.file, m.line, m.s)
}

func New(text, file string, line int) error {
    return &myError{text, file, line}
}

func main() {
    err := do()
    switch err := err.(type) {
    case nil:
        // no error
    case *myError:
        fmt.Printf("error occurred: %v\n", err.Error())
    default:
        // unknown error
    }
}

func do() error {
    return New("something error", "main.go", 27)
}

Error Type可以使用断言转换成这个类型,来获取上下文信息,但是Error Type仍然存在与其他包产生耦合的问题。

3、Opaque errors(非透明错误处理)

Opaque erros只关心返回的错误而不是错误内容。

func fn() error {
    i, err := do()
    if err != nil {
        return err
    }
    // do something
}

Opaque errors的好处减少了代码间的耦合性,但是在很多时候仍然需要确认返回错误的性质,以便进一步处理,比如网络请求是超时还是返回错误,此时推荐使用断言错误的行为,而不是断言错误的类型。

例如:
定义timeout接口,IsTimeout方法判断错误的行为。

type timeout interface {
    Timeout() bool
}

func IsTimeout(err error) bool {
    t, ok := err.(timeout)
    return ok && t.Timeout()
}

当相关error实现Timeout方法后即可判断其存在超时行为。

if IsTimeout(err) {
    // time out
}

这里关键,可以在不到入定义错误的包或者不了解底层类型时判断错误的性质。

4、Wrap Error

Wrap Error可以将最底层的错误保存的同时添加更多上下文信息,需要判断错误性质时可以获取底层错误。

Wrap Error目前有两种方式,一种是标准库的errors

包,另外一种就是第三方包github.com/pkg/errors(下称pkg/errors),在目前go 1.x版本下,pkg/errors相比于errors包可以记录堆栈信息,方便调试。

package main

import (
    "errors"
    "fmt"
    pkError "github.com/pkg/errors"
)

func main() {
    err := service()
    fmt.Printf("casue err: %+v\n", pkError.Cause(err))
    fmt.Printf("err: %+v", err)
}

func service() error {
    err := biz()
    return pkError.WithMessage(err, "this is service")
}

func biz() error {
    err := dao()
    return pkError.WithMessage(err, "this is biz")
}

func dao() error {
    err := errors.New("dao err")
    return pkError.Wrap(err, "this is dao")
}

// 打印信息
// casue err: dao err
// err: dao err
// this is dao
// main.dao
// .../main.go:26 main.biz
// .../main.go:20 main.service
// .../main.go:15 main.main
// .../main.go:10 runtime.main
// /usr/local/go/src/runtime/proc.go:225 runtime.goexit
// /usr/local/go/src/runtime/asm_amd64.s:1371
// this is biz
// this is service

这里假设dao层获取对数据的操作错误后,使用pkg/errors将错误Wrap后返回,biz层获取到dao层错误使用WithMessage增加相关上下文,最终传递上层可打印出上下文和堆栈信息,并且可以使用Cause(或者Unwrap)获取最底层错误。

当需要比较错误时,可使用IsAs方法,如果错误已经被Wrap,它们会获取最底层错误进行比较。

func Is(err, target error) bool
func As(err, target interface{}) bool

As相比IsAs会在err的底层错误与target相等时,将err赋值给target

Wrap Error小结:
Wrap Error可以保留Sentinal Error特性的同时携带上下文信息,一定程度上也兼容了Error Type特性,在日常编码中推荐使用这种方式。

注意事项:

1. 如果函数返回了value,error,必须判定error,否则返回的value不可用,除非你连value也不关心
2. error应该只被处理一次(打印日志也算是对error的一种处理),要么降级处理,要么向上层返回error
3. 当与第三方库或者基础库交互时,可以将第一次错误Wrap(只能Wrap一次,否则将重复打印堆栈信息)向上返回,最终在顶层打印日志,可以避免日志的重复打印及调试方便。
4.使用哪种error的处理方式,最终由使用场景来决定。

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!