Go 中自定义 error 的详细说明
基础自定义 error
最简单的自定义 error 方式:
// 方式1:使用字符串 |
携带结构体数据的自定义 error
// 定义携带额外数据的错误结构体 |
实现 Unwrap 方法
Unwrap 方法用于错误链的解包,配合 errors.Unwrap() 和 errors.Is()/errors.As() 使用:
// 基础错误类型 |
多级错误包装
// 定义多个错误类型 |
携带多种数据的复杂错误示例
// 复杂的业务错误类型 |
使用 errors.Is 和 errors.As 的最佳实践
// 定义哨兵错误 |
完整的生产级示例
package main |
关键要点总结
- 实现 Error() 方法:任何实现了
Error() string方法的类型都可以作为 error - Unwrap 方法:返回被包装的错误,用于错误链的解包
- 携带数据:可以在结构体中添加任意字段存储额外信息
- errors.Is/As:用于检查错误类型和获取具体错误实例
- 错误包装:通过嵌套 error 字段和实现 Unwrap 创建错误链
- 哨兵错误:预定义的错误变量,便于错误比较
这种自定义错误的方式可以让你在应用程序中构建丰富的错误处理系统,便于调试和错误分类处理。
errors.Is 和 errors.As 的详细对比
相同点
- 都属于 errors 包:都是 Go 1.13 引入的错误处理函数
- 都支持错误链:两者都会自动调用
Unwrap()方法遍历整个错误链 - 都用于错误检查:都是用来检查和处理错误的辅助函数
- 都不修改原始错误:只是检查错误,不会改变原始错误
不同点
| 特性 | errors.Is | errors.As |
|---|---|---|
| 主要用途 | 检查错误是否等于特定值 | 检查错误是否属于特定类型,并提取该类型的值 |
| 比较方式 | 值比较(通过 == 或自定义的 Is 方法) |
类型断言和赋值 |
| 返回值 | bool | bool(同时将匹配的错误赋值给目标变量) |
| 目标参数 | error 类型的目标值 | 指针类型的目标变量(接收匹配的错误) |
| 典型场景 | 检查是否为特定哨兵错误 | 获取错误中的结构化数据 |
详细示例说明
基础示例 - 准备错误类型
package main |
errors.Is 示例
func demonstrateErrorsIs() { |
errors.As 示例
func demonstrateErrorsAs() { |
结合 Is 和 As 的完整示例
func demonstrateCombined() { |
自定义 Is 方法
// 带有自定义 Is 方法的错误类型 |
实际应用场景示例
func practicalExample() { |
执行所有示例
func main() { |
总结
| 方面 | errors.Is | errors.As |
|---|---|---|
| 核心用途 | 值相等性检查 | 类型匹配和数据提取 |
| 目标类型 | error 值 | 指向错误类型的指针 |
| 匹配机制 | 比较错误值或调用 Is 方法 | 类型断言 |
| 适用场景 | 哨兵错误检查 | 获取结构化错误数据 |
| 错误链处理 | 自动解包直到匹配值 | 自动解包直到类型匹配 |
| 常见用法 | errors.Is(err, ErrNotFound) |
var myErr *MyType; errors.As(err, &myErr) |
选择建议:
- 当只需要知道错误是否为特定哨兵错误时,使用 errors.Is
- 当需要获取错误中的额外数据(如字段名、错误码等)时,使用 errors.As
- 在复杂的错误处理中,可以组合使用两者
Unwrap 方法的详细说明
Unwrap 方法基础
Unwrap 方法是 Go 1.13 引入的错误处理机制中的核心概念,它用于从包装错误中解包出内部的原始错误。
// Unwrap 方法的定义 |
Unwrap 的基本用法
实现 Unwrap 方法
package main |
Unwrap 的自动调用机制
errors.Is 中的自动解包
func demonstrateIsUnwrap() { |
errors.As 中的自动解包
type DetailedError struct { |
多错误包装(多重 Unwrap)
同时包装多个错误
// 包装多个错误的类型 |
自定义 Unwrap 逻辑
条件性解包
type ConditionalError struct { |
错误链中的跳过逻辑
type SkipError struct { |
与 fmt.Errorf 的 %w 配合
func demonstrateFmtErrorf() { |
实际应用场景
错误堆栈跟踪
type StackError struct { |
错误重试机制
type RetryableError struct { |
错误链遍历的高级用法
// 自定义错误链遍历 |
Unwrap 方法的限制和注意事项
func demonstrateUnwrapLimitations() { |
总结
| 特性 | 说明 |
|---|---|
| 核心作用 | 从包装错误中提取内部错误 |
| 自动调用 | errors.Is 和 errors.As 会自动调用 Unwrap |
| 手动调用 | errors.Unwrap(err) 手动解包一层 |
| 返回值 | 返回被包装的错误,如果没有则返回 nil |
| 链式结构 | 可以形成错误链,便于逐层处理 |
| 实现要求 | 返回 error 接口类型 |
| 最佳实践 | 始终处理 nil 情况,避免循环包装 |
Unwrap 的主要应用场景:
- 错误分类和处理(通过 errors.Is/As)
- 错误堆栈跟踪
- 错误上下文的添加和剥离
- 重试机制中的错误分析
- 日志记录和调试
设计原则:Unwrap 应该返回被包装的原始错误,保持错误链的完整性,使得上层可以访问到完整的错误信息。
捕获 panic 并打印堆栈
Go语言中没有传统的 try-catch 异常捕获机制,而是使用 panic 和 recover 来处理意料之外的异常情况。recover 必须配合 defer 使用才能生效。
基础示例
package main |
输出示例:开始执行风险操作...
程序捕获到了panic: runtime error: index out of range [10] with length 0
堆栈信息如下:
goroutine 1 [running]:
runtime/debug.Stack()
/usr/local/go/src/runtime/debug/stack.go:24 +0x65
runtime/debug.PrintStack()
/usr/local/go/src/runtime/debug/stack.go:16 +0x19
main.main.func1()
/path/to/your/main.go:17 +0x6b
panic({0x...?})
/usr/local/go/src/runtime/panic.go:914 +0x...
main.riskyFunction(...)
/path/to/your/main.go:8
main.main()
/path/to/your/main.go:22 +0x...
在这个例子中,recover() 捕获了 panic,然后使用 debug.PrintStack() 将完整的调用堆栈打印出来,这样你就能清楚地看到是哪一行代码引发了问题。
跨协程的限制
需要注意的是,panic 只能被同一个 goroutine 中的 recover 捕获。如果在子协程中发生了 panic,主协程中的 recover 是无能为力的。
package main |
为普通错误(error)添加和打印堆栈
在Go的最佳实践中,更推荐使用返回 error 的方式来处理可预见的错误。但标准库的 error 默认不带堆栈信息,我们可以借助第三方库来丰富错误信息,方便调试。
使用 github.com/pkg/errors 库
这个库是非常流行的错误处理工具,可以轻松地为错误附加堆栈信息。
安装:
go get github.com/pkg/errors
使用
Wrap包装错误,附加堆栈:
当你在函数调用链中遇到错误时,可以使用errors.Wrap或errors.Wrapf来包装它,这会自动捕获当前的堆栈信息。使用
%+v打印完整堆栈:
在使用fmt.Printf等函数打印错误时,使用%+v格式化动词可以输出包含堆栈的详细信息。
package main |
输出效果:
使用 %+v 会输出类似于下面的信息,包含完整的函数调用链和文件名、行号,这对于定位错误非常关键。
执行step1失败: 数据库连接失败 |
总结
| 场景 | 捕获/创建方式 | 打印堆栈方式 | 关键点 |
|---|---|---|---|
捕获 panic |
defer + recover() |
debug.PrintStack() |
recover 必须在 defer 中且与 panic 同协程 |
增强 error |
github.com/pkg/errors.Wrap |
fmt.Printf("%+v", err) |
在错误传播的起点使用 Wrap 捕获堆栈 |
在你的开发中,是遇到了一个需要捕获的 panic 场景,还是希望为普通的业务错误添加上下文堆栈?如果能分享更具体的例子,我可以帮你看看哪种方式更合适。