目录

go 如何打印错误时代码堆栈信息(使用库 pkg error 包)

背景

刚接手Go项目时,遇到过一次生产环境报错——日志只打印“查询用户失败”,没有任何调用链路信息。翻了半天代码才定位到是数据库查询的子函数抛错。

打印错误 error 日志时,仅有错误信息,没有错误代码的堆栈信息,这在问题排查上比较困难,具体代码如下

package main

import (
    "errors"
    "fmt"
)
func main() {
    err := NewErr("this is err msg")
    fmt.Printf("%v\n", err)
}

this is err msg

没有错误的堆栈信息(也就是代码的调用执行信息),只是单纯的根据 log 信息有时候很难排查具体出现问题的代码块,如果有堆栈信息,那么就可以快速定位代码出现问题的代码行

那么如何才能实现打印数据,同时输出 trace 调用栈信息呢?答案:使用 第三方库 github.com/pkg/errors 替换官方库 errors 包(基本上是无损替换,pkg errors 包兼容 errors 包)

pkg errors 包

Go原生的error类型本质是接口,默认实现(如errors.Newfmt.Errorf)只能存储错误信息,无法记录错误发生的调用栈和上下文。

使用 github.com/pkg/errors 替换 errors 包,可以无缝替换,因为 pkg/errors 包的 api 和 errors 几乎一致,但是功能更加强大

(一)用Wrap包装错误,附加堆栈

errors.Wrap函数包装原始错误,该函数会自动记录当前调用栈。代码:

package main

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

func queryDB() error {
    // 原始错误(如第三方库返回的原生error)
    rawErr := errors.New("数据库连接超时")
    return rawErr
}

func getUser() error {
    if err := queryDB(); err != nil {
        // 关键:用Wrap包装,附加上下文和堆栈
        return errors.Wrap(err, "获取用户信息失败")
    }
    return nil
}

func main() {
    if err := getUser(); err != nil {
        // 打印错误信息(含上下文)
        fmt.Println("错误信息:", err)
        // 打印完整堆栈
        fmt.Println("完整堆栈:", errors.WithStack(err))
    }
}

运行查看结果

运行后会输出两段内容,重点看“完整堆栈”部分,能清晰看到错误从queryDB抛出,经过getUser传递到main的完整链路:

错误信息: 获取用户信息失败: 数据库连接超时
完整堆栈: 数据库连接超时
github.com/your-project/demo/queryDB
        /Users/xxx/go/src/your-project/demo/main.go:10
github.com/your-project/demo/getUser
        /Users/xxx/go/src/your-project/demo/main.go:16
github.com/your-project/demo/main
        /Users/xxx/go/src/your-project/demo/main.go:23
runtime.main
        /usr/local/go/src/runtime/proc.go:250
runtime.goexit
        /usr/local/go/src/runtime/asm_amd64.s:1594

此时你会发现堆栈的信息只能追踪到:err = errors.WithStack(err)

所以:堆栈只能追踪到:只有在使用了 pkg/errors 包封装的代码位置

第三方库的代码绝大多数都没有使用 github.com/pkg/errors

⚠️⚠️:

所以在使用第三方库的时,err 应该用 errors.WithStack(err) 或者 errors.Wrap(err,“自定义错误信息”) 进行封装,然后再往上抛

这里有个小技巧:用%+v格式化输出,能更简洁地打印带堆栈的错误,不用单独调用WithStack

// 简化打印方式,%+v自动输出堆栈
fmt.Printf("错误详情:%+v\n", err)

(二)嵌套错误的堆栈追溯(多层调用场景)

当错误经过多层函数传递时,只需在每一层用errors.Wrap包装,最终打印时会展示完整的调用链。比如“API层→业务层→数据层”的三层调用:

package main

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

// 数据层:模拟数据库错误
func queryDB() error {
    return errors.New("查询SQL语法错误")
}

// 业务层:处理业务逻辑
func getUserService(userID int) error {
    if err := queryDB(); err != nil {
        return errors.Wrap(err, "业务层查询用户失败")
    }
    return nil
}

// API层:接收请求
func getUserAPI(userID int) error {
    if err := getUserService(userID); err != nil {
        return errors.Wrap(err, "API层处理请求失败")
    }
    return nil
}

func main() {
    if err := getUserAPI(1001); err != nil {
        fmt.Printf("错误详情:%+v\n", err)
    }
}

运行结果会清晰展示三层调用的错误链路,定位问题时能直接找到最底层的SQL错误:

错误详情: API层处理请求失败: 业务层查询用户失败: 查询SQL语法错误
github.com/your-project/demo/queryDB
        /Users/xxx/go/src/your-project/demo/main.go:10
github.com/your-project/demo/getUserService
        /Users/xxx/go/src/your-project/demo/main.go:16
github.com/your-project/demo/getUserAPI
        /Users/xxx/go/src/your-project/demo/main.go:23
github.com/your-project/demo/main
        /Users/xxx/go/src/your-project/demo/main.go:30

(三)自定义错误类型附加堆栈

有时需要自定义错误(比如携带错误码),只需让自定义类型实现error接口,再用errors.WithStack附加堆栈即可:

package main

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

// 自定义错误类型:携带错误码和信息
type BusinessError struct {
    Code    int    // 错误码
    Message string // 错误信息
}

// 实现error接口
func (e *BusinessError) Error() string {
    return fmt.Sprintf("错误码:%d,信息:%s", e.Code, e.Message)
}

func createOrder() error {
    // 模拟参数错误
    if 1 == 1 { // 测试条件
        // 自定义错误 + 附加堆栈
        return errors.WithStack(&BusinessError{
            Code:    400,
            Message: "订单金额不能为负数",
        })
    }
    return nil
}

func main() {
    if err := createOrder(); err != nil {
        fmt.Printf("错误详情:%+v\n", err)
    }
}

(四)只附加上下文,不重复生成堆栈

如果已经用Wrap生成过堆栈,后续传递时只需用errors.WithMessage附加上下文,避免堆栈冗余。比如:

func getUser() error {
    if err := queryDB(); err != nil {
        // 第一次包装,生成堆栈
        return errors.Wrap(err, "获取用户失败")
    }
    return nil
}

func apiHandler() error {
    if err := getUser(); err != nil {
        // 后续附加上下文,不重复生成堆栈
        return errors.WithMessage(err, "处理用户查询API失败")
    }
    return nil
}

注意实现

pkg/errors时,很多问题都和“堆栈重复”“错误判断”有关,分享实战中踩过的坑:

(一)重复Wrap导致堆栈冗余

原因:同一错误被多次Wrap,会生成多段重复的堆栈,日志冗余。

解决办法:只在错误首次发生时用Wrap(生成堆栈),后续传递用WithMessage(只加上下文)。

(二)用==判断包装后的错误失效

原因Wrap后的错误是pkg/errors的包装类型,和原生错误直接用==判断会失败。

解决办法:用errors.Is函数判断原始错误:

代码如下 :

var ErrDBTimeout = errors.New("数据库连接超时")

func queryDB() error {
    return errors.Wrap(ErrDBTimeout, "queryDB失败")
}

func main() {
    err := queryDB()
    // 错误用法:==判断失效
    if err == ErrDBTimeout {
        fmt.Println("数据库超时") // 不会执行
    }
    // 正确用法:用errors.Is
    if errors.Is(err, ErrDBTimeout) {
        fmt.Println("数据库超时") // 正常执行
    }
}

(三)堆栈中没有行号信息

原因:编译时加了-w(去掉调试信息)或-s(去掉符号表)参数,导致堆栈无法关联行号。

解决办法:生产环境编译时保留调试信息(若担心文件过大,可通过其他方式优化):

# 正确编译命令保留调试信息
go build -o demo main.go

# 错误编译命令丢失行号
go build -w -s -o demo main.go

(四)堆栈信息过长,包含无关框架代码

解决办法:用errors.Cause获取原始错误,或在日志框架中配置堆栈过滤(如zap可通过配置过滤runtime相关行)。

总结

pkg/errors的核心价值是“让错误可追溯”,关键用法可总结为3个核心函数:

  • errors.Wrap(err, msg):首次包装错误,附加堆栈和上下文(必用)

  • errors.WithMessage(err, msg):后续传递时附加上下文,不重复生成堆栈(常用)

  • errors.Is(err, target):判断包装错误中的原始错误(避坑关键)

适用场景:几乎所有Go后端开发场景,尤其是微服务、多层级调用的项目。唯一需要注意的是:高频调用的函数(如百万级循环中的错误处理)需评估性能,普通业务场景完全无需担心。

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/go-pkg-err/

备用原文链接: https://blog.fiveyoboy.com/articles/go-pkg-err/