[tldr] GO语言异常处理

在开发中, 处理异常是很重要的, 考虑各种错误情况并且提出对应的解决办法是保证不出大BUG的重要之处.

error in go

GO语言的异常是很简单的, 只需要实现Error函数接口即可

go 复制代码
func (e ErrA) Error() string {
	return "ErrA"
}

过于简单的实现, 和C一样

errors

这是GO的一个库, 专门用来处理异常

Go语言的标准库一般会在后面加一个s代表是处理这种类型的标准库, 例如strings

异常处理

error类别判断

类型断言实现类别判断

Go语言提供了类型断言的方式, 针对接口, 可以实现不同类型之间的相互转换

这就是为什么errA需要是error类型, 因为是接口类型才能转换

go 复制代码
	var errA error = ErrA{}
	var errB error = ErrB{}

	fmt.Println(errA.Error())
	fmt.Println(errB.Error())

	if e, ok := errA.(ErrB); ok {
		fmt.Println(e)
		fmt.Println("found ErrB")
	} else {
		fmt.Println("not found ErrB")
	}
  • 这个简单好用, 如果需要嵌套的异常处理, 那么应该选择使用这个.
  • 当然可以使用switch语句针对多种错误类型进行处理, 针对多个类型的时候使用这个方式可以更好的处理
go 复制代码
func main() {
	var errA error = ErrA{}
	var errB error = ErrB{wrappedErr: errA}

	for err := errB; err != nil; err = errors.Unwrap(err) {
		fmt.Println(err)
		switch err.(type) {
		case ErrA:
			fmt.Println("ErrA")
		case ErrB:
			fmt.Println("ErrB")
		default:
			fmt.Println("default")
		}
		fmt.Println()
	}
}

上述代码是使用switch配合类型断言来处理异常

errors函数类别判断

通过errors.Is或者errors.As函数实现类别的判断

Go语言官方提供的异常处理, 也只是简单的封装.

  • 这个函数的本质是检查这个error是否包含某个error(一个error可以包含多个error, 嵌套error)
go 复制代码
	var errA error = ErrA{}
	var errB error = ErrB{}

	fmt.Println(errA.Error())
	fmt.Println(errB.Error())

	if errors.Is(errA, errB) {
		fmt.Println("errA is errB")
	} else {
		fmt.Println("errA is not errB")
	}

使用这个函数让代码看起来十分的简洁

  • 使用errors提供的函数在处理嵌套的error的时候可能存在问题, 我们在下面讨论

嵌套error

有些时候我们需要把error一层一层wrap起来然后向上抛出异常进行处理. 像是多层的Throw错误.

errors库提供了Unwrap函数, 如果error实现了这个接口, 那么就可以实现error嵌套

go 复制代码
package main

import (
	"errors"
	"fmt"
)

type ErrA struct {
	wrappedErr error
}

func (e ErrA) Error() string {
	return fmt.Sprintf("ErrA -> %v", e.wrappedErr)
}

func (e ErrA) Unwrap() error {
	return e.wrappedErr
}

type ErrB struct {
	wrappedErr error
}

func (e ErrB) Error() string {
	return fmt.Sprintf("ErrB -> %v", e.wrappedErr)
}

func (e ErrB) Unwrap() error {
	return e.wrappedErr
}

func main() {
	errA := ErrA{}
	errB := ErrB{}

	fmt.Println(errA.Error())
	fmt.Println(errB.Error())

	if errors.Is(errA, ErrB{}) {
		fmt.Println("errA is ErrB")
	} else {
		fmt.Println("errA is not ErrB")
	}

	fmt.Println()

	var wrapErr error = ErrA{wrappedErr: ErrB{}}
	for err := wrapErr; err != nil; err = errors.Unwrap(err) {
		fmt.Println(err)
	}
}

可以使用for循环语句来解开error

但是这个for循环拿出来的error使用errors.Is函数是无法准确识别当前的error属于哪个类别的, 因为所有被wrap的error类别都可以被识别到.

当然也可以通过errors库提供的Join函数实现合并多个error的功能, var wrappedErr error = errors.Join(errA, errB)语句提供了合并多个error的方式.

合并之后的error实现的Unwrap接口是返回[]error的, 但是在errors库使用的时候, 会被视为同一个error

可以通过errors.As或者errors.Is判断当前的错误是否包含某一个特定的错误

errors处理嵌套error

当然可以使用errors库来处理嵌套的error, 因为只要符合error接口规范的都可以使用errors进行处理

但是存在一些不方便的地方, 比如, 一个嵌套ErrA的ErrB错误对象, 同时可以被认为是ErrA和ErrB类型. 下面是一个例子

go 复制代码
package main

import (
	"errors"
	"fmt"
)

type ErrA struct {
	wrappedErr error
}

func (e ErrA) Error() string {
	return fmt.Sprintf("ErrA -> %v", e.wrappedErr)
}

func (e ErrA) Unwrap() error {
	return e.wrappedErr
}

type ErrB struct {
	wrappedErr error
}

func (e ErrB) Error() string {
	return fmt.Sprintf("ErrB -> %v", e.wrappedErr)
}

func (e ErrB) Unwrap() error {
	return e.wrappedErr
}

func main() {
	var errA error = ErrA{}
	var errB error = ErrB{wrappedErr: errA}

	for err := errB; err != nil; err = errors.Unwrap(err) {
		fmt.Println(err)
		if errors.Is(err, errA) {
			fmt.Println("errA is the cause of the error")
		}
		fmt.Println()
	}
}

结果是

无法识别当前这一层的error是ErrB

造成这个问题的原因是, errors.Is的源码实现如下:

go 复制代码
func is(err, target error, targetComparable bool) bool {
	for {
		if targetComparable && err == target {
			return true
		}
		if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
			return true
		}
		switch x := err.(type) {
		case interface{ Unwrap() error }:
			err = x.Unwrap()
			if err == nil {
				return false
			}
		case interface{ Unwrap() []error }:
			for _, err := range x.Unwrap() {
				if is(err, target, targetComparable) {
					return true
				}
			}
			return false
		default:
			return false
		}
	}
}

如果遇上可以Unwrap的接口类型直接再次Unwrap去找是否存在一个类型是符合条件的

对此, 需要使用类型断言的方式来判断错误类型

go 复制代码
func main() {
	var errA error = ErrA{}
	var errB error = ErrB{wrappedErr: errA}

	for err := errB; err != nil; err = errors.Unwrap(err) {
		fmt.Println(err)
		switch err.(type) {
		case ErrA:
			fmt.Println("ErrA")
		case ErrB:
			fmt.Println("ErrB")
		default:
			fmt.Println("default")
		}
		fmt.Println()
	}
}

效果如下:

成功识别出了错误的类型

  • 如果需要准确识别错误的具体类型, 而不是检查这个报错包含哪些错误的话, 应该使用这个

参考

GO语言的errors库
Go语言(golang)新发布的1.13中的Error Wrapping深度分析

相关推荐
HyggeBest7 小时前
Golang 并发原语 Sync Once
后端·go
zhuyasen1 天前
当Go框架拥有“大脑”,Sponge框架集成AI开发项目,从“手写”到一键“生成”业务逻辑代码
后端·go·ai编程
写代码的比利1 天前
Kratos 对接口进行加密转发处理的两个方法
go
chenqianghqu1 天前
goland编译过程加载dll路径时出现失败
go
马里嗷1 天前
Go 1.25 标准库更新
后端·go·github
郭京京1 天前
go语言redis中使用lua脚本
redis·go·lua
心月狐的流火号1 天前
分布式锁技术详解与Go语言实现
分布式·微服务·go
一个热爱生活的普通人2 天前
使用 Makefile 和 Docker 简化你的 Go 服务部署流程
后端·go