关于go:golang中的错误处理

46次阅读

共计 5977 个字符,预计需要花费 15 分钟才能阅读完成。

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 {// ...}
}

正文完
 0