[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深度分析

相关推荐
研究司马懿26 分钟前
【云原生】Gateway API高级功能
云原生·go·gateway·k8s·gateway api
梦想很大很大14 小时前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
lekami_兰19 小时前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘1 天前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤1 天前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt111 天前
AI DDD重构实践
go
Grassto3 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto5 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室6 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题6 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo