写点什么

golang 中的错误处理

作者:六月的
  • 2022-11-02
    上海
  • 本文字数:4262 字

    阅读完需:约 14 分钟

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 hellopanic: 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.gohellorecover:  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}
// 1type ForbiddenError struct { Err error}
// 2func (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.goForbidden: 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.go500 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.go400 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.go500 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 {        // ...    }}
复制代码


用户头像

六月的

关注

还未添加个人签名 2019-07-23 加入

还未添加个人简介

评论

发布
暂无评论
golang中的错误处理_golang_六月的_InfoQ写作社区