Go自定义错误用法详解

所有的期待和失望都是因为你把自己看得太重要了

Posted by yishuifengxiao on 2026-03-02

Go 中自定义 error 的详细说明

基础自定义 error

最简单的自定义 error 方式:

// 方式1:使用字符串
type MyError string

func (e MyError) Error() string {
return string(e)
}

// 使用
func doSomething() error {
return MyError("something went wrong")
}

携带结构体数据的自定义 error

// 定义携带额外数据的错误结构体
type ValidationError struct {
Field string
Value interface{}
Message string
Code int
}

// 实现 error 接口
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on field %s: %s (value: %v, code: %d)",
e.Field, e.Message, e.Value, e.Code)
}

// 使用示例
func ValidateAge(age int) error {
if age < 0 {
return &ValidationError{
Field: "age",
Value: age,
Message: "age cannot be negative",
Code: 400,
}
}
if age > 150 {
return &ValidationError{
Field: "age",
Value: age,
Message: "age exceeds maximum limit",
Code: 400,
}
}
return nil
}

func main() {
err := ValidateAge(-5)
if err != nil {
// 类型断言获取具体错误信息
if vErr, ok := err.(*ValidationError); ok {
fmt.Printf("Field: %s, Code: %d\n", vErr.Field, vErr.Code)
}
fmt.Println(err)
}
}

实现 Unwrap 方法

Unwrap 方法用于错误链的解包,配合 errors.Unwrap()errors.Is()/errors.As() 使用:

// 基础错误类型
type DatabaseError struct {
Table string
Operation string
Err error // 底层错误
}

func (e *DatabaseError) Error() string {
if e.Err != nil {
return fmt.Sprintf("database error on %s %s: %v",
e.Operation, e.Table, e.Err)
}
return fmt.Sprintf("database error on %s %s", e.Operation, e.Table)
}

// 实现 Unwrap 方法 - 返回被包装的错误
func (e *DatabaseError) Unwrap() error {
return e.Err
}

// 使用示例
func QueryUser(id int) error {
err := executeQuery("SELECT * FROM users WHERE id = ?", id)
if err != nil {
return &DatabaseError{
Table: "users",
Operation: "query",
Err: err, // 包装原始错误
}
}
return nil
}

多级错误包装

// 定义多个错误类型
type NetworkError struct {
Host string
Port int
Err error
}

func (e *NetworkError) Error() string {
return fmt.Sprintf("network error connecting to %s:%d: %v",
e.Host, e.Port, e.Err)
}

func (e *NetworkError) Unwrap() error {
return e.Err
}

type TimeoutError struct {
Duration time.Duration
Err error
}

func (e *TimeoutError) Error() string {
return fmt.Sprintf("timeout after %v: %v", e.Duration, e.Err)
}

func (e *TimeoutError) Unwrap() error {
return e.Err
}

// 多层包装
func FetchData() error {
err := connectToService()
if err != nil {
return &NetworkError{
Host: "api.example.com",
Port: 443,
Err: &TimeoutError{
Duration: 30 * time.Second,
Err: err,
},
}
}
return nil
}

func main() {
err := FetchData()

// 使用 errors.Is 检查特定错误类型
var timeoutErr *TimeoutError
if errors.As(err, &timeoutErr) {
fmt.Printf("Timeout occurred: %v\n", timeoutErr.Duration)
}

// 逐层解包
for err != nil {
fmt.Printf("Layer: %v\n", err)
err = errors.Unwrap(err)
}
}

携带多种数据的复杂错误示例

// 复杂的业务错误类型
type BusinessError struct {
Code string // 错误码
Message string // 用户友好消息
Details map[string]interface{} // 详细数据
StackTrace []string // 堆栈信息
InnerError error // 内部错误
Timestamp time.Time // 时间戳
}

func NewBusinessError(code, message string, inner error) *BusinessError {
return &BusinessError{
Code: code,
Message: message,
Details: make(map[string]interface{}),
StackTrace: captureStack(2), // 自定义捕获堆栈的函数
InnerError: inner,
Timestamp: time.Now(),
}
}

func (e *BusinessError) Error() string {
if e.InnerError != nil {
return fmt.Sprintf("[%s] %s: %v (time: %v)",
e.Code, e.Message, e.InnerError, e.Timestamp)
}
return fmt.Sprintf("[%s] %s (time: %v)", e.Code, e.Message, e.Timestamp)
}

func (e *BusinessError) Unwrap() error {
return e.InnerError
}

// 添加额外方法
func (e *BusinessError) AddDetail(key string, value interface{}) *BusinessError {
e.Details[key] = value
return e
}

func (e *BusinessError) GetDetail(key string) interface{} {
return e.Details[key]
}

// 使用示例
func ProcessOrder(orderID string) error {
// 模拟处理过程
err := validateOrder(orderID)
if err != nil {
return NewBusinessError("VALIDATION_FAILED", "order validation failed", err).
AddDetail("order_id", orderID).
AddDetail("validation_time", time.Now())
}

err = saveOrder(orderID)
if err != nil {
return NewBusinessError("SAVE_FAILED", "failed to save order", err).
AddDetail("order_id", orderID).
AddDetail("retry_count", 3)
}

return nil
}

func main() {
err := ProcessOrder("ORD-123")
if err != nil {
// 类型断言获取完整错误信息
if bizErr, ok := err.(*BusinessError); ok {
fmt.Printf("Error Code: %s\n", bizErr.Code)
fmt.Printf("Message: %s\n", bizErr.Message)
fmt.Printf("Details: %+v\n", bizErr.Details)
fmt.Printf("Time: %v\n", bizErr.Timestamp)

// 检查内部错误
if bizErr.InnerError != nil {
fmt.Printf("Inner Error: %v\n", bizErr.InnerError)
}
}
}
}

使用 errors.Is 和 errors.As 的最佳实践

// 定义哨兵错误
var (
ErrNotFound = errors.New("resource not found")
ErrUnauthorized = errors.New("unauthorized access")
ErrInvalidInput = errors.New("invalid input")
)

// 带数据的错误类型
type QueryError struct {
Query string
Err error
}

func (e *QueryError) Error() string {
return fmt.Sprintf("query error: %s: %v", e.Query, e.Err)
}

func (e *QueryError) Unwrap() error {
return e.Err
}

func (e *QueryError) Is(target error) bool {
// 自定义 Is 方法,使错误匹配更灵活
t, ok := target.(*QueryError)
if !ok {
return false
}
return e.Query == t.Query
}

// 使用示例
func main() {
err := someFunction()

// 1. 使用 errors.Is 检查特定错误
if errors.Is(err, ErrNotFound) {
fmt.Println("Resource not found, handling...")
}

// 2. 使用 errors.As 获取具体错误类型的数据
var qErr *QueryError
if errors.As(err, &qErr) {
fmt.Printf("Failed query: %s\n", qErr.Query)
fmt.Printf("Original error: %v\n", qErr.Err)
}

// 3. 链式错误处理
switch {
case errors.Is(err, ErrNotFound):
handleNotFound()
case errors.Is(err, ErrUnauthorized):
handleUnauthorized()
default:
// 处理其他错误
var bizErr *BusinessError
if errors.As(err, &bizErr) {
fmt.Printf("Business error: %s, code: %s\n",
bizErr.Message, bizErr.Code)
}
}
}

完整的生产级示例

package main

import (
"errors"
"fmt"
"runtime"
"time"
)

// AppError 是应用程序的基础错误类型
type AppError struct {
Code string
Message string
Op string // 操作名称
Err error // 内部错误
Fields map[string]interface{}
Stack []string
Timestamp time.Time
}

func NewAppError(code, message, op string, err error) *AppError {
return &AppError{
Code: code,
Message: message,
Op: op,
Err: err,
Fields: make(map[string]interface{}),
Stack: captureStack(),
Timestamp: time.Now(),
}
}

func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%s] %s: %s: %v", e.Code, e.Op, e.Message, e.Err)
}
return fmt.Sprintf("[%s] %s: %s", e.Code, e.Op, e.Message)
}

func (e *AppError) Unwrap() error {
return e.Err
}

func (e *AppError) WithField(key string, value interface{}) *AppError {
e.Fields[key] = value
return e
}

// 捕获堆栈信息
func captureStack() []string {
var stack []string
for i := 2; ; i++ {
pc, file, line, ok := runtime.Caller(i)
if !ok {
break
}
fn := runtime.FuncForPC(pc)
stack = append(stack, fmt.Sprintf("%s:%d %s", file, line, fn.Name()))
}
return stack
}

// 使用示例
func main() {
// 创建错误
err := NewAppError("E001", "user not found", "GetUser",
errors.New("database: record not exists")).
WithField("user_id", 123).
WithField("query", "SELECT * FROM users WHERE id=123")

// 处理错误
fmt.Println("Error:", err)

// 解包
var appErr *AppError
if errors.As(err, &appErr) {
fmt.Printf("Code: %s\n", appErr.Code)
fmt.Printf("Operation: %s\n", appErr.Op)
fmt.Printf("Fields: %+v\n", appErr.Fields)
fmt.Printf("Timestamp: %v\n", appErr.Timestamp)
fmt.Printf("Stack trace:\n")
for _, frame := range appErr.Stack {
fmt.Printf(" %s\n", frame)
}
}
}

关键要点总结

  1. 实现 Error() 方法:任何实现了 Error() string 方法的类型都可以作为 error
  2. Unwrap 方法:返回被包装的错误,用于错误链的解包
  3. 携带数据:可以在结构体中添加任意字段存储额外信息
  4. errors.Is/As:用于检查错误类型和获取具体错误实例
  5. 错误包装:通过嵌套 error 字段和实现 Unwrap 创建错误链
  6. 哨兵错误:预定义的错误变量,便于错误比较

这种自定义错误的方式可以让你在应用程序中构建丰富的错误处理系统,便于调试和错误分类处理。


errors.Is 和 errors.As 的详细对比

相同点

  1. 都属于 errors 包:都是 Go 1.13 引入的错误处理函数
  2. 都支持错误链:两者都会自动调用 Unwrap() 方法遍历整个错误链
  3. 都用于错误检查:都是用来检查和处理错误的辅助函数
  4. 都不修改原始错误:只是检查错误,不会改变原始错误

不同点

特性 errors.Is errors.As
主要用途 检查错误是否等于特定值 检查错误是否属于特定类型,并提取该类型的值
比较方式 值比较(通过 == 或自定义的 Is 方法) 类型断言和赋值
返回值 bool bool(同时将匹配的错误赋值给目标变量)
目标参数 error 类型的目标值 指针类型的目标变量(接收匹配的错误)
典型场景 检查是否为特定哨兵错误 获取错误中的结构化数据

详细示例说明

基础示例 - 准备错误类型

package main

import (
"errors"
"fmt"
)

// 哨兵错误定义
var (
ErrNotFound = errors.New("resource not found")
ErrTimeout = errors.New("operation timeout")
)

// 自定义错误类型
type ValidationError struct {
Field string
Value interface{}
Msg string
}

func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s (value: %v)",
e.Field, e.Msg, e.Value)
}

type DatabaseError struct {
Table string
Op string
Err error
}

func (e *DatabaseError) Error() string {
return fmt.Sprintf("database error on %s %s: %v", e.Op, e.Table, e.Err)
}

func (e *DatabaseError) Unwrap() error {
return e.Err
}

errors.Is 示例

func demonstrateErrorsIs() {
fmt.Println("=== errors.Is 示例 ===")

// 示例1: 基本哨兵错误比较
err1 := ErrNotFound
if errors.Is(err1, ErrNotFound) {
fmt.Println("✓ 错误是 ErrNotFound")
}

// 示例2: 包装错误链中的哨兵错误
err2 := &DatabaseError{
Table: "users",
Op: "query",
Err: ErrNotFound,
}

// errors.Is 会自动解包,能检测到包装在内的 ErrNotFound
if errors.Is(err2, ErrNotFound) {
fmt.Println("✓ 包装错误链中包含 ErrNotFound")
}

// 示例3: 多层包装
err3 := fmt.Errorf("外层错误: %w",
fmt.Errorf("中间层错误: %w", ErrTimeout))

if errors.Is(err3, ErrTimeout) {
fmt.Println("✓ 多层包装后仍能检测到 ErrTimeout")
}

// 示例4: errors.Is 不会匹配类型,只匹配值
validationErr := &ValidationError{
Field: "age",
Value: -1,
Msg: "must be positive",
}

// 下面的比较返回 false,因为类型不同
if errors.Is(validationErr, ErrNotFound) {
fmt.Println("不会执行")
} else {
fmt.Println("✗ validationErr 不等于 ErrNotFound")
}
}

errors.As 示例

func demonstrateErrorsAs() {
fmt.Println("\n=== errors.As 示例 ===")

// 示例1: 提取自定义错误类型
err1 := &ValidationError{
Field: "email",
Value: "invalid-email",
Msg: "invalid format",
}

var valErr *ValidationError
if errors.As(err1, &valErr) {
fmt.Printf("✓ 提取到 ValidationError: Field=%s, Value=%v, Msg=%s\n",
valErr.Field, valErr.Value, valErr.Msg)
}

// 示例2: 从错误链中提取特定类型
err2 := &DatabaseError{
Table: "orders",
Op: "insert",
Err: &ValidationError{
Field: "order_id",
Value: "",
Msg: "cannot be empty",
},
}

var validationErr *ValidationError
if errors.As(err2, &validationErr) {
fmt.Printf("✓ 从错误链中提取到 ValidationError: %+v\n", validationErr)
}

// 示例3: 多层包装中提取
err3 := fmt.Errorf("API错误: %w",
fmt.Errorf("业务错误: %w", err2))

var dbErr *DatabaseError
if errors.As(err3, &dbErr) {
fmt.Printf("✓ 从多层包装中提取到 DatabaseError: Table=%s, Op=%s\n",
dbErr.Table, dbErr.Op)
}

// 示例4: errors.As 的第二个参数必须是**非空指针**
// 错误用法:
// var wrongType *ValidationError
// if errors.As(err1, wrongType) // 错误:wrongType 是 nil

// 正确用法:
var correctType *ValidationError
if errors.As(err1, &correctType) { // 传递指针的指针
fmt.Println("✓ 正确使用 errors.As")
}
}

结合 Is 和 As 的完整示例

func demonstrateCombined() {
fmt.Println("\n=== 结合使用 errors.Is 和 errors.As ===")

// 创建复杂的错误场景
baseErr := &ValidationError{
Field: "username",
Value: "",
Msg: "required field",
}

wrappedErr := &DatabaseError{
Table: "users",
Op: "create",
Err: fmt.Errorf("操作失败: %w", baseErr),
}

// 处理错误
err := wrappedErr

// 1. 先检查是否为特定哨兵错误
if errors.Is(err, ErrNotFound) {
fmt.Println("处理 not found 情况")
} else {
fmt.Println("不是 not found 错误")
}

// 2. 尝试提取 DatabaseError
var dbErr *DatabaseError
if errors.As(err, &dbErr) {
fmt.Printf("数据库错误: %s %s\n", dbErr.Op, dbErr.Table)

// 3. 进一步检查内部的错误
if errors.Is(dbErr.Err, ErrNotFound) {
fmt.Println("数据库错误中包含 not found")
}

// 4. 尝试提取内部的 ValidationError
var valErr *ValidationError
if errors.As(dbErr.Err, &valErr) {
fmt.Printf("验证错误详情: %s=%v, %s\n",
valErr.Field, valErr.Value, valErr.Msg)
}
}
}

自定义 Is 方法

// 带有自定义 Is 方法的错误类型
type HTTPError struct {
StatusCode int
Message string
Err error
}

func (e *HTTPError) Error() string {
return fmt.Sprintf("HTTP %d: %s", e.StatusCode, e.Message)
}

func (e *HTTPError) Unwrap() error {
return e.Err
}

// 自定义 Is 方法,允许更灵活的比较
func (e *HTTPError) Is(target error) bool {
// 如果目标也是 HTTPError,比较状态码
if t, ok := target.(*HTTPError); ok {
return e.StatusCode == t.StatusCode
}
// 如果目标是哨兵错误 ErrNotFound,检查是否为 404
if target == ErrNotFound {
return e.StatusCode == 404
}
return false
}

func demonstrateCustomIs() {
fmt.Println("\n=== 自定义 Is 方法示例 ===")

err := &HTTPError{
StatusCode: 404,
Message: "User not found",
Err: ErrNotFound,
}

// 使用自定义 Is 逻辑
if errors.Is(err, ErrNotFound) {
fmt.Println("✓ 通过自定义 Is,404 错误被视为 ErrNotFound")
}

// 比较 HTTPError 之间的状态码
targetErr := &HTTPError{StatusCode: 404}
if errors.Is(err, targetErr) {
fmt.Println("✓ 两个 404 错误被视为相等")
}
}

实际应用场景示例

func practicalExample() {
fmt.Println("\n=== 实际应用场景 ===")

// 模拟一个需要错误分类的业务逻辑
err := processOrder("order-123")
if err != nil {
// 场景1: 使用 errors.Is 判断错误类型
switch {
case errors.Is(err, ErrNotFound):
fmt.Println("处理方案: 重新查询订单")
case errors.Is(err, ErrTimeout):
fmt.Println("处理方案: 重试操作")
}

// 场景2: 使用 errors.As 获取详细错误信息
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("处理方案: 返回字段 '%s' 的错误信息给用户\n", valErr.Field)
}

// 场景3: 同时使用
var httpErr *HTTPError
if errors.As(err, &httpErr) {
fmt.Printf("HTTP错误 %d: %s\n", httpErr.StatusCode, httpErr.Message)

if httpErr.StatusCode == 500 {
fmt.Println("处理方案: 记录详细日志并报警")
}
}
}
}

func processOrder(orderID string) error {
// 模拟一些错误
return &DatabaseError{
Table: "orders",
Op: "get",
Err: &ValidationError{
Field: "order_id",
Value: orderID,
Msg: "invalid format",
},
}
}

执行所有示例

func main() {
demonstrateErrorsIs()
demonstrateErrorsAs()
demonstrateCombined()
demonstrateCustomIs()
practicalExample()
}

总结

方面 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 方法的定义
type wrapper interface {
Unwrap() error
}

Unwrap 的基本用法

实现 Unwrap 方法

package main

import (
"errors"
"fmt"
)

// 基础包装错误
type WrappedError struct {
Msg string
Err error // 被包装的原始错误
}

func (e *WrappedError) Error() string {
return fmt.Sprintf("%s: %v", e.Msg, e.Err)
}

// 实现 Unwrap 方法 - 返回被包装的错误
func (e *WrappedError) Unwrap() error {
return e.Err
}

func main() {
originalErr := errors.New("原始错误")
wrappedErr := &WrappedError{
Msg: "包装错误",
Err: originalErr,
}

// 手动解包
unwrappedErr := errors.Unwrap(wrappedErr)
fmt.Println("解包后的错误:", unwrappedErr)
fmt.Println("是否为原始错误:", unwrappedErr == originalErr) // true
}

Unwrap 的自动调用机制

errors.Is 中的自动解包

func demonstrateIsUnwrap() {
fmt.Println("=== errors.Is 自动解包 ===")

originalErr := errors.New("disk full")

// 多层包装
err1 := &WrappedError{Msg: "存储错误", Err: originalErr}
err2 := &WrappedError{Msg: "系统错误", Err: err1}
err3 := fmt.Errorf("致命错误: %w", err2) // fmt.Errorf 的 %w 也会创建包装错误

// errors.Is 会自动调用 Unwrap 遍历整个错误链
if errors.Is(err3, originalErr) {
fmt.Println("✓ 找到了原始错误: disk full")
}

// 等价于手动解包的过程:
fmt.Println("\n手动解包过程:")
current := err3
for current != nil {
fmt.Printf(" 当前错误: %v\n", current)
current = errors.Unwrap(current)
}
}

errors.As 中的自动解包

type DetailedError struct {
Code int
Message string
Err error
}

func (e *DetailedError) Error() string {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}

func (e *DetailedError) Unwrap() error {
return e.Err
}

type TimeoutError struct {
Duration int
Err error
}

func (e *TimeoutError) Error() string {
return fmt.Sprintf("timeout after %dms: %v", e.Duration, e.Err)
}

func (e *TimeoutError) Unwrap() error {
return e.Err
}

func demonstrateAsUnwrap() {
fmt.Println("\n=== errors.As 自动解包 ===")

// 创建多层错误链
timeoutErr := &TimeoutError{
Duration: 5000,
Err: errors.New("connection refused"),
}

detailedErr := &DetailedError{
Code: 500,
Message: "服务调用失败",
Err: timeoutErr,
}

// 外层包装
finalErr := fmt.Errorf("API错误: %w", detailedErr)

// errors.As 会自动解包找到匹配的类型
var foundTimeout *TimeoutError
if errors.As(finalErr, &foundTimeout) {
fmt.Printf("✓ 找到 TimeoutError: 时长=%dms\n", foundTimeout.Duration)
}

var foundDetailed *DetailedError
if errors.As(finalErr, &foundDetailed) {
fmt.Printf("✓ 找到 DetailedError: 错误码=%d\n", foundDetailed.Code)
}
}

多错误包装(多重 Unwrap)

同时包装多个错误

// 包装多个错误的类型
type MultiError struct {
Msg string
Err1 error
Err2 error
current int // 用于遍历
}

func (e *MultiError) Error() string {
return fmt.Sprintf("%s: (%v, %v)", e.Msg, e.Err1, e.Err2)
}

// Unwrap 返回下一个错误,实现遍历多个错误
func (e *MultiError) Unwrap() error {
if e.current == 0 {
e.current++
return e.Err1
}
if e.current == 1 {
e.current++
return e.Err2
}
return nil
}

// 更常见的多错误包装是使用切片
type AggregateError struct {
Message string
Errors []error
index int
}

func (e *AggregateError) Error() string {
return fmt.Sprintf("%s: %d errors occurred", e.Message, len(e.Errors))
}

// Unwrap 返回下一个错误,直到遍历完所有错误
func (e *AggregateError) Unwrap() error {
if e.index >= len(e.Errors) {
return nil
}
err := e.Errors[e.index]
e.index++
return err
}

func demonstrateMultiUnwrap() {
fmt.Println("\n=== 多重 Unwrap 示例 ===")

aggErr := &AggregateError{
Message: "验证失败",
Errors: []error{
errors.New("字段 'name' 不能为空"),
errors.New("字段 'age' 必须大于0"),
errors.New("字段 'email' 格式错误"),
},
}

// 手动遍历所有错误
fmt.Println("遍历所有错误:")
current := error(aggErr)
for current != nil {
fmt.Printf(" - %v\n", current)
current = errors.Unwrap(current)
}
}

自定义 Unwrap 逻辑

条件性解包

type ConditionalError struct {
Msg string
Err error
ShouldUnwrap bool
}

func (e *ConditionalError) Error() string {
return e.Msg
}

// 根据条件决定是否解包
func (e *ConditionalError) Unwrap() error {
if e.ShouldUnwrap {
return e.Err
}
return nil
}

func demonstrateConditionalUnwrap() {
fmt.Println("\n=== 条件性解包 ===")

err1 := &ConditionalError{
Msg: "条件性错误",
Err: errors.New("内部错误"),
ShouldUnwrap: true,
}

err2 := &ConditionalError{
Msg: "另一个条件性错误",
Err: errors.New("内部错误"),
ShouldUnwrap: false,
}

fmt.Println("err1 解包:", errors.Unwrap(err1)) // 返回内部错误
fmt.Println("err2 解包:", errors.Unwrap(err2)) // 返回 nil
}

错误链中的跳过逻辑

type SkipError struct {
Msg string
Err error
Skip bool
}

func (e *SkipError) Error() string {
if e.Skip {
return fmt.Sprintf("[SKIP] %s", e.Msg)
}
return e.Msg
}

func (e *SkipError) Unwrap() error {
// 如果 Skip 为 true,跳过当前错误直接返回内部的内部错误
if e.Skip && e.Err != nil {
if skip, ok := e.Err.(*SkipError); ok {
return skip.Unwrap()
}
}
return e.Err
}

func demonstrateSkipUnwrap() {
fmt.Println("\n=== 跳过解包示例 ===")

inner := &SkipError{
Msg: "内部错误",
Err: errors.New("原始错误"),
}

middle := &SkipError{
Msg: "中间错误",
Err: inner,
Skip: true, // 标记为跳过
}

outer := &SkipError{
Msg: "外部错误",
Err: middle,
}

// 解包时会跳过 middle 直接到 inner
unwrapped := errors.Unwrap(outer)
fmt.Printf("外层解包: %v\n", unwrapped) // 直接得到 inner

// 再解包得到原始错误
final := errors.Unwrap(unwrapped)
fmt.Printf("最终错误: %v\n", final)
}

与 fmt.Errorf 的 %w 配合

func demonstrateFmtErrorf() {
fmt.Println("\n=== fmt.Errorf 的 %w ===")

baseErr := errors.New("数据库连接失败")

// 使用 %w 包装错误
err1 := fmt.Errorf("查询失败: %w", baseErr)

// %w 创建的包装错误也实现了 Unwrap
unwrapped := errors.Unwrap(err1)
fmt.Printf("解包后: %v\n", unwrapped)

// 多层 %w 包装
err2 := fmt.Errorf("业务逻辑错误: %w",
fmt.Errorf("服务调用错误: %w", baseErr))

// errors.Is 能自动遍历
if errors.Is(err2, baseErr) {
fmt.Println("✓ 找到了原始错误")
}

// 对比 %v (不包装) 和 %w (包装)
err3 := fmt.Errorf("不包装: %v", baseErr) // 不包装
err4 := fmt.Errorf("包装: %w", baseErr) // 包装

fmt.Printf("不包装的解包结果: %v\n", errors.Unwrap(err3)) // nil
fmt.Printf("包装的解包结果: %v\n", errors.Unwrap(err4)) // baseErr
}

实际应用场景

错误堆栈跟踪

type StackError struct {
Err error
File string
Line int
Func string
}

func (e *StackError) Error() string {
return fmt.Sprintf("%s:%d [%s] %v", e.File, e.Line, e.Func, e.Err)
}

func (e *StackError) Unwrap() error {
return e.Err
}

// 创建带堆栈的错误
func WithStack(err error) error {
if err == nil {
return nil
}

// 获取调用信息
pc, file, line, ok := runtime.Caller(1)
if !ok {
return err
}

funcName := runtime.FuncForPC(pc).Name()
return &StackError{
Err: err,
File: file,
Line: line,
Func: funcName,
}
}

func demonstrateStackError() {
fmt.Println("\n=== 错误堆栈跟踪 ===")

func level3() error {
return errors.New("底层错误")
}

func level2() error {
err := level3()
return WithStack(err) // 包装堆栈信息
}

func level1() error {
err := level2()
return fmt.Errorf("业务处理失败: %w", err)
}

err := level1()

// 遍历错误链打印堆栈
fmt.Println("完整错误链:")
current := err
for current != nil {
if stackErr, ok := current.(*StackError); ok {
fmt.Printf(" %s:%d %s\n", stackErr.File, stackErr.Line, stackErr.Func)
}
fmt.Printf(" %v\n", current)
current = errors.Unwrap(current)
}
}

错误重试机制

type RetryableError struct {
Err error
RetryCount int
MaxRetries int
}

func (e *RetryableError) Error() string {
return fmt.Sprintf("可重试错误 (尝试 %d/%d): %v",
e.RetryCount, e.MaxRetries, e.Err)
}

func (e *RetryableError) Unwrap() error {
return e.Err
}

// 判断是否需要重试
func ShouldRetry(err error) bool {
var retryErr *RetryableError
if errors.As(err, &retryErr) {
return retryErr.RetryCount < retryErr.MaxRetries
}
return false
}

func demonstrateRetryableError() {
fmt.Println("\n=== 错误重试机制 ===")

err := &RetryableError{
Err: errors.New("网络超时"),
RetryCount: 2,
MaxRetries: 3,
}

fmt.Printf("原始错误: %v\n", err)

// 包装后仍然能识别
wrapped := fmt.Errorf("操作失败: %w", err)

if ShouldRetry(wrapped) {
fmt.Println("✓ 需要重试")
// 获取具体信息
var retryErr *RetryableError
if errors.As(wrapped, &retryErr) {
fmt.Printf(" 已重试 %d 次, 最多 %d 次\n",
retryErr.RetryCount, retryErr.MaxRetries)
}
}

// 解包获取原始错误
fmt.Printf("解包后的错误: %v\n", errors.Unwrap(wrapped))
}

错误链遍历的高级用法

// 自定义错误链遍历
func WalkErrors(err error, fn func(error) bool) {
for err != nil {
if !fn(err) {
break
}
err = errors.Unwrap(err)
}
}

// 收集所有错误
func CollectErrors(err error) []error {
var errors []error
WalkErrors(err, func(e error) bool {
errors = append(errors, e)
return true
})
return errors
}

// 查找特定类型的错误
func FindError[T error](err error) T {
var zero T
for err != nil {
if e, ok := err.(T); ok {
return e
}
err = errors.Unwrap(err)
}
return zero
}

func demonstrateAdvancedUnwrap() {
fmt.Println("\n=== 高级错误遍历 ===")

// 创建复杂错误链
err := &DetailedError{
Code: 400,
Message: "验证失败",
Err: &TimeoutError{
Duration: 1000,
Err: &WrappedError{
Msg: "连接错误",
Err: errors.New("connection refused"),
},
},
}

// 收集所有错误
allErrors := CollectErrors(err)
fmt.Println("错误链中的所有错误:")
for i, e := range allErrors {
fmt.Printf(" %d: %v\n", i+1, e)
}

// 查找特定类型
timeoutErr := FindError[*TimeoutError](err)
if timeoutErr != nil {
fmt.Printf("\n找到 TimeoutError: %v\n", timeoutErr)
}
}

Unwrap 方法的限制和注意事项

func demonstrateUnwrapLimitations() {
fmt.Println("\n=== Unwrap 的注意事项 ===")

// 1. Unwrap 只能返回一个 error
// 2. 如果要包装多个错误,需要特殊处理(如 AggregateError)
// 3. 循环包装会导致无限递归
// 4. nil 检查很重要

// 错误示例:循环包装
type CircularError struct {
Err error
Name string
}

func (e *CircularError) Error() string { return e.Name }
func (e *CircularError) Unwrap() error { return e.Err }

// 创建循环引用
err1 := &CircularError{Name: "err1"}
err2 := &CircularError{Name: "err2", Err: err1}
err1.Err = err2 // 循环!

// 这会导致无限循环
fmt.Println("注意:循环包装会导致无限循环")

// 安全使用 Unwrap
safeErr := &WrappedError{Msg: "安全错误", Err: nil}
fmt.Printf("安全解包 nil: %v\n", errors.Unwrap(safeErr)) // 返回 nil
}

总结

特性 说明
核心作用 从包装错误中提取内部错误
自动调用 errors.Is 和 errors.As 会自动调用 Unwrap
手动调用 errors.Unwrap(err) 手动解包一层
返回值 返回被包装的错误,如果没有则返回 nil
链式结构 可以形成错误链,便于逐层处理
实现要求 返回 error 接口类型
最佳实践 始终处理 nil 情况,避免循环包装

Unwrap 的主要应用场景:

  1. 错误分类和处理(通过 errors.Is/As)
  2. 错误堆栈跟踪
  3. 错误上下文的添加和剥离
  4. 重试机制中的错误分析
  5. 日志记录和调试

设计原则:Unwrap 应该返回被包装的原始错误,保持错误链的完整性,使得上层可以访问到完整的错误信息。


捕获 panic 并打印堆栈

Go语言中没有传统的 try-catch 异常捕获机制,而是使用 panicrecover 来处理意料之外的异常情况。recover 必须配合 defer 使用才能生效。

基础示例

package main

import (
"fmt"
"runtime/debug"
)

func riskyFunction() {
// 模拟一个会引发panic的操作,例如数组越界
var slice []int
fmt.Println(slice[10]) // 这里会触发panic
}

func main() {
// 使用defer和recover来捕获可能发生的panic
defer func() {
if r := recover(); r != nil {
fmt.Println("程序捕获到了panic:", r)
// 关键步骤:打印堆栈信息
fmt.Println("堆栈信息如下:")
debug.PrintStack()
}
}()

fmt.Println("开始执行风险操作...")
riskyFunction()
fmt.Println("这行代码不会被执行,因为上面panic了")
}

输出示例

开始执行风险操作...
程序捕获到了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

import (
"fmt"
"time"
)

func main() {
defer func() {
if r := recover(); r != nil {
// 这里的 recover 捕获不到子协程的 panic
fmt.Println("主协程的recover:", r)
}
}()

go func() {
defer func() {
// 正确的做法:在子协程内部进行 recover
if r := recover(); r != nil {
fmt.Println("子协程捕获到panic:", r)
}
}()
panic("子协程出错了")
}()

time.Sleep(time.Second)
fmt.Println("程序正常结束")
}

为普通错误(error)添加和打印堆栈

在Go的最佳实践中,更推荐使用返回 error 的方式来处理可预见的错误。但标准库的 error 默认不带堆栈信息,我们可以借助第三方库来丰富错误信息,方便调试。

使用 github.com/pkg/errors

这个库是非常流行的错误处理工具,可以轻松地为错误附加堆栈信息。

  1. 安装

    go get github.com/pkg/errors
  2. 使用 Wrap 包装错误,附加堆栈
    当你在函数调用链中遇到错误时,可以使用 errors.Wraperrors.Wrapf 来包装它,这会自动捕获当前的堆栈信息。

  3. 使用 %+v 打印完整堆栈
    在使用 fmt.Printf 等函数打印错误时,使用 %+v 格式化动词可以输出包含堆栈的详细信息。

package main

import (
"fmt"
"github.com/pkg/errors"
)

func step2() error {
// 模拟一个底层错误
return errors.New("数据库连接失败")
}

func step1() error {
err := step2()
if err != nil {
// 关键点:用Wrap包装错误,并添加上下文信息,同时捕获堆栈
return errors.Wrap(err, "执行step1失败")
}
return nil
}

func main() {
err := step1()
if err != nil {
fmt.Println("普通打印 (只包含错误信息):")
fmt.Printf("%s\n", err) // 或 %v,不包含堆栈

fmt.Println("\n详细打印 (包含堆栈信息):")
fmt.Printf("%+v\n", err) // 包含完整堆栈
}
}

输出效果
使用 %+v 会输出类似于下面的信息,包含完整的函数调用链和文件名、行号,这对于定位错误非常关键。

执行step1失败: 数据库连接失败
main.step1
/path/to/your/main.go:15
main.main
/path/to/your/main.go:24
runtime.main
/usr/local/go/src/runtime/proc.go:250
...

总结

场景 捕获/创建方式 打印堆栈方式 关键点
捕获 panic defer + recover() debug.PrintStack() recover 必须在 defer 中且与 panic 同协程
增强 error github.com/pkg/errors.Wrap fmt.Printf("%+v", err) 在错误传播的起点使用 Wrap 捕获堆栈

在你的开发中,是遇到了一个需要捕获的 panic 场景,还是希望为普通的业务错误添加上下文堆栈?如果能分享更具体的例子,我可以帮你看看哪种方式更合适。