golang 优雅错误处理规范(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) } 编写一个正则表达式 调用Compile ,解析正则表达式 ,如果成功 ,返回用于匹配文本的 Regexp 对象 。否则返回错误 利用正则,在输入的字符串中 ,获取所有的匹配字符可以看到如果上面正则解析失败是可以继续往下执行的 ,但是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 、包装错误
错误包装是将错误包装或者打包在一个包装容器中,这样的话我们就可以追溯到源错误 。错误包装的主要作用就是:
为错误添加上下文 将错误标记为特定类型的错误我们可以看一个访问数据库的例子:
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) } } 访问数据库时我们返回了原始的错误信息 到上层我们添加了一些自定义的上下文信息 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) } } 首先我们自定义了ForbiddenError的错误类型 我们实现了error接口 访问数据库抛出原始错误 上层返回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) } } 使用%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 denied4 、错误值判断
在代码中或者mysql库或者io库中我们经常会看到这样的全局错误:
var ErrCourseware = errors.New("courseware")这种错误我们称之为哨兵错误 。一般数据库没查到ErrNoRows或者io读到了EOF错误 ,这些特定的错误可以帮助我们做一些特殊的处理。
一般我们会直接用==号判断错误值 ,但是就像上面的如果错误被包装哪我们就不好去判断了 。好在errors包中提供了errors.Is方法 ,通过递归调用Unwrap判断错误链中是否与目标错误相匹配的错误值:
if err != nil { if errors.Is(err, ErrCourseware) { // ... } else { // ... } }创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!