关于go:golang中的错误处理

0.1、索引

https://waterflow.link/articles/1666716727236

1、panic

当咱们执行panic的时候会完结上面的流程:

package main

import "fmt"

func main() {
    fmt.Println("hello")
    panic("stop")
    fmt.Println("world")
}
go run 9.go 
hello
panic: stop

然而panic也是能够捕捉的,咱们能够应用defer和recover实现:

package main

import "fmt"

func main() {

    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recover: ", r)
        }
    }()

    fmt.Println("hello")
    panic("stop")
    fmt.Println("world")
}
go run 9.go
hello
recover:  stop

那什么时候适宜panic呢?在 Go 中,panic 用于示意真正的异样,例如程序谬误。咱们常常会在一些内置包外面看到panic的身影。

比方strings.Repeat反复返回一个由字符串 s 的计数正本组成的新字符串:

func Repeat(s string, count int) string {
    if count == 0 {
        return ""
    }

    // 
    if count < 0 {
        panic("strings: negative Repeat count")
    } else if len(s)*count/count != len(s) {
        panic("strings: Repeat count causes overflow")
    }

    ...
}

咱们能够看到当反复的次数小于0或者反复count次之后s的长度溢出,程序会间接panic,而不是返回谬误。这时因为strings包限度了error的应用,所以在程序谬误时会间接panic。

还有一个例子是对于正则表达式的例子:

package main

import (
    "fmt"
    "regexp"
)

func main() {
    pattern := "a[a-z]b*" // 1
    compile, err := regexp.Compile(pattern) // 2
    if err != nil { // 2
        fmt.Println("compile err: ", err)
        return
    }
  // 3
    allString := compile.FindAllString("acbcdadb", 3)
    fmt.Println(allString)

}
  1. 编写一个正则表达式
  2. 调用Compile,解析正则表达式,如果胜利,返回用于匹配文本的 Regexp 对象。否则返回谬误
  3. 利用正则,在输出的字符串中,获取所有的匹配字符

能够看到如果下面正则解析失败是能够持续往下执行的,然而regexp包中还有另外一个办法MustCompile:

func MustCompile(str string) *Regexp {
    regexp, err := Compile(str)
    if err != nil {
        panic(`regexp: Compile(` + quote(str) + `): ` + err.Error())
    }
    return regexp
}

这个办法阐明正则的解析是强依赖的,如果解析谬误,间接panic完结程序。用户能够依据理论状况抉择。

然而理论开发中咱们还是要审慎应用panic,因为它会使程序完结运行(除非咱们调用defer recover)

2、包装谬误

谬误包装是将谬误包装或者打包在一个包装容器中,这样的话咱们就能够追溯到源谬误。谬误包装的次要作用就是:

  1. 为谬误增加上下文
  2. 将谬误标记为特定类型的谬误

咱们能够看一个拜访数据库的例子:

package main

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

type Courseware struct {
    Id int64
    Code string
    Name string
}

func getCourseware(id int64) (*Courseware, error) {
    courseware, err := getFromDB(id)
    if err != nil {
        return nil, errors.Wrap(err, "六月的想拜访这个课件") // 2
    }
    return courseware, nil
}

func getFromDB(id int64) (*Courseware, error) {
    return nil, errors.New("permission denied") // 1
}

func main() {
    _, err := getCourseware(11)
    if err != nil {
        fmt.Println(err)
    }
}
  1. 拜访数据库时咱们返回了原始的错误信息
  2. 到下层咱们增加了一些自定义的上下文信息
go run 9.go
六月的想拜访这个课件: permission denied

当然咱们也能够将谬误包装成咱们自定义类型的谬误,咱们略微批改下下面的例子:

package main

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

type Courseware struct {
    Id int64
    Code string
    Name string
}

// 1
type ForbiddenError struct {
    Err error
}

// 2
func (e *ForbiddenError) Error() string {
    return "Forbidden: " + e.Err.Error()
}

func getCourseware(id int64) (*Courseware, error) {
    courseware, err := getFromDB(id)
    if err != nil {
        return nil, &ForbiddenError{err} // 4
    }
    return courseware, nil
}

func getFromDB(id int64) (*Courseware, error) {
    return nil, errors.New("permission denied") // 3
}

func main() {
    _, err := getCourseware(11)
    if err != nil {
        fmt.Println(err)
    }
}
  1. 首先咱们自定义了ForbiddenError的谬误类型
  2. 咱们实现了error接口
  3. 拜访数据库抛出原始谬误
  4. 下层返回ForbiddenError类型的谬误
go run 9.go
Forbidden: permission denied

当然咱们也能够不必创立自定义谬误的类型,去包装谬误增加上下文:

package main

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

type Courseware struct {
    Id int64
    Code string
    Name string
}


func getCourseware(id int64) (*Courseware, error) {
    courseware, err := getFromDB(id)
    if err != nil {
        return nil, fmt.Errorf("another wrap err: %w", err) // 1
    }
    return courseware, nil
}

func getFromDB(id int64) (*Courseware, error) {
    return nil, errors.New("permission denied")
}

func main() {
    _, err := getCourseware(11)
    if err != nil {
        fmt.Println(err)
    }
}
  1. 应用%w包装谬误

应用这的益处是咱们能够追溯到源谬误,从而不便咱们做一些非凡的解决。

还有一种形式是应用:

return nil, fmt.Errorf("another wrap err: %v", err)

%v的形式不会包装谬误,所以无奈追溯到源谬误,但往往有时候咱们会抉择这种形式,而不必%w的形式。%w的形式尽管能包装源谬误,但往往咱们会通过源谬误去做一些解决,如果源谬误被批改,那包装这个源谬误的相干谬误都须要做响应变动。

3、谬误类型判断

咱们扩大一下下面查问课件的例子。当初咱们有这样的判断,如果传进来的id不非法咱们返回400谬误,如果查询数据库报错咱们返回500谬误,咱们能够像上面这样写:

package main

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

type Courseware struct {
    Id int64
    Code string
    Name string
}

type ForbiddenError struct {
    Err error
}

func (e *ForbiddenError) Error() string {
    return "Forbidden: " + e.Err.Error()
}

func getCourseware(id int64) (*Courseware, error) {
    if id <= 0 {
        return nil, fmt.Errorf("invalid id: %d", id)
    }
    courseware, err := getFromDB(id)
    if err != nil {
        return nil, &ForbiddenError{err}
    }
    return courseware, nil
}

func getFromDB(id int64) (*Courseware, error) {
    return nil, errors.New("permission denied")
}

func main() {
    _, err := getCourseware(500) // 咱们能够批改这里的id看下打印的构造
    if err != nil {
        switch err := err.(type) {
        case *ForbiddenError:
            fmt.Println("500 err: ", err)
        default:
            fmt.Println("400 err: ", err)
        }
    }
}
go run 9.go
500 err:  Forbidden: permission denied

这样看起来如同也没什么问题,当初咱们略微批改下代码,把下面ForbiddenError包装一下:

package main

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

type Courseware struct {
    Id int64
    Code string
    Name string
}

type ForbiddenError struct {
    Err error
}

func (e *ForbiddenError) Error() string {
    return "Forbidden: " + e.Err.Error()
}

func getCourseware(id int64) (*Courseware, error) {
    if id <= 0 {
        return nil, fmt.Errorf("invalid id: %d", id)
    }
    courseware, err := getFromDB(id)
    if err != nil {
        return nil, fmt.Errorf("wrap err: %w", &ForbiddenError{err}) // 这里包装了一层谬误
    }
    return courseware, nil
}

func getFromDB(id int64) (*Courseware, error) {
    return nil, errors.New("permission denied")
}

func main() {
    _, err := getCourseware(500)
    if err != nil {
        switch err := err.(type) {
        case *ForbiddenError:
            fmt.Println("500 err: ", err)
        default:
            fmt.Println("400 err: ", err)
        }
    }
}
go run 9.go
400 err:  wrap err: Forbidden: permission denied

能够看到咱们的Forbidden谬误进到了400外面,这并不是咱们想要的后果。之所以会这样,是因为在ForbiddenError的里面又包装了一层Error谬误,应用类型断言的时候判断进去的是Error谬误,所以进到了400分支。

这里咱们能够应用errors.As办法,它会递归调用Unwrap办法,找到谬误链中第一个与target匹配的办法:

package main

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

type Courseware struct {
    Id int64
    Code string
    Name string
}

type ForbiddenError struct {
    Err error
}

func (e *ForbiddenError) Error() string {
    return "Forbidden: " + e.Err.Error()
}

func getCourseware(id int64) (*Courseware, error) {
    if id <= 0 {
        return nil, fmt.Errorf("invalid id: %d", id)
    }
    courseware, err := getFromDB(id)
    if err != nil {
        return nil, fmt.Errorf("wrap err: %w", &ForbiddenError{err})
    }
    return courseware, nil
}

func getFromDB(id int64) (*Courseware, error) {
    return nil, errors.New("permission denied")
}

func main() {
    _, err := getCourseware(500)
    if err != nil {
        var f *ForbiddenError // 这里实现了*ForbiddenError接口,不然会panic
        if errors.As(err, &f) { // 找到匹配的谬误
            fmt.Println("500 err: ", err)
        } else {
            fmt.Println("400 err: ", err)
        }
    }
}
go run 9.go
500 err:  wrap err: Forbidden: permission denied

4、谬误值判断

在代码中或者mysql库或者io库中咱们常常会看到这样的全局谬误:

var ErrCourseware = errors.New("courseware")

这种谬误咱们称之为哨兵谬误。个别数据库没查到ErrNoRows或者io读到了EOF谬误,这些特定的谬误能够帮忙咱们做一些非凡的解决。

个别咱们会间接用==号判断谬误值,然而就像下面的如果谬误被包装哪咱们就不好去判断了。好在errors包中提供了errors.Is办法,通过递归调用Unwrap判断谬误链中是否与指标谬误相匹配的谬误值:

if err != nil {
    if errors.Is(err, ErrCourseware) {
        // ...
    } else {
        // ...
    }
}

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理