• 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")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    go run 9.go 
    hello
    panic: stop
    
    • 1
    • 2
    • 3

    但是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")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    go run 9.go
    hello
    recover:  stop
    
    • 1
    • 2
    • 3

    那什么时候适合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")
    	}
    
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    我们可以看到当重复的次数小于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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这个方法说明正则的解析是强依赖的,如果解析错误,直接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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    1. 访问数据库时我们返回了原始的错误信息
    2. 到上层我们添加了一些自定义的上下文信息
    go run 9.go
    六月的想访问这个课件: permission denied
    
    • 1
    • 2

    当然我们也可以将错误包装成我们自定义类型的错误,我们稍微修改下上面的例子:

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    1. 首先我们自定义了ForbiddenError的错误类型
    2. 我们实现了error接口
    3. 访问数据库抛出原始错误
    4. 上层返回ForbiddenError类型的错误
    go run 9.go
    Forbidden: permission denied
    
    • 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, 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    1. 使用%w包装错误

    使用这的好处是我们可以追溯到源错误,从而方便我们做一些特殊的处理。

    还有一种方式是使用:

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

    %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)
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    go run 9.go
    500 err:  Forbidden: permission denied
    
    • 1
    • 2

    这样看起来好像也没什么问题,现在我们稍微修改下代码,把上面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)
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    go run 9.go
    400 err:  wrap err: Forbidden: permission denied
    
    • 1
    • 2

    可以看到我们的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)
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    go run 9.go
    500 err:  wrap err: Forbidden: permission denied
    
    • 1
    • 2

    4、错误值判断

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

    var ErrCourseware = errors.New("courseware")
    
    • 1

    这种错误我们称之为哨兵错误。一般数据库没查到ErrNoRows或者io读到了EOF错误,这些特定的错误可以帮助我们做一些特殊的处理。

    一般我们会直接用==号判断错误值,但是就像上面的如果错误被包装哪我们就不好去判断了。好在errors包中提供了errors.Is方法,通过递归调用Unwrap判断错误链中是否与目标错误相匹配的错误值:

    if err != nil {
        if errors.Is(err, ErrCourseware) {
            // ...
        } else {
            // ...
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
  • 相关阅读:
    CTFHub技能树web之XSS
    StringBuilder的底层实现原理
    GAN原理及代码实现
    MySQL 主从复制与读写分离
    osgEarth示例分析——osgearth_cluster
    【Netty】1-2. TCP通信与Netty基本介绍
    UE4 C++设计模式:策略模式(Strategy Pattern)
    最新Python大数据之Excel进阶
    暴力破解常见服务学习
    Spring Boot常用注解
  • 原文地址:https://blog.csdn.net/liuyuede123/article/details/127650800