Go错误处理以返回error值为主,panic/recover仅用于不可恢复的编程错误;应使用fmt.Errorf("xxx: %w", err)包装错误以保留错误链,用errors.Is/As进行判断,首次或边界处记录完整错误链。

Go里没有try-catch,panic和recover不是错误处理主力
Go语言不支持传统意义上的“捕获并重新抛出错误”,因为它的错误模型基于返回值而非异常机制。error是普通接口类型,函数通过返回error值显式传达失败,调用方必须主动检查。试图用panic/recover模拟Java式的异常传播,不仅违背Go设计哲学,还会导致栈信息丢失、难以调试、性能开销大,且无法跨goroutine安全recover。
真正该做的,是把错误当数据来传递和增强:
-
if err != nil后不要直接return err了事,而是考虑是否需要补充上下文 - 避免裸写
return errors.New("xxx")或return fmt.Errorf("xxx"),除非是底层原始错误 - 跨层传播时,优先用
fmt.Errorf("xxx: %w", err)包装,保留原始错误链
用%w动词包装错误才能实现“重新抛出”语义
Go 1.13引入的%w格式动词,是实现错误增强与可追溯传播的核心。它把原错误嵌入新错误中,使errors.Is和errors.As能穿透多层包装做判断。
func readFile(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read config file %q: %w", path, err)
}
// ...
return nil
}
这样调用方可以:
立即学习“go语言免费学习笔记(深入)”;
- 用
errors.Is(err, os.ErrNotExist)判断是否是文件不存在(即使被包装过) - 用
errors.As(err, &pathErr)提取底层*os.PathError - 用
errors.Unwrap(err)手动解包,或遍历整个错误链
注意:%w只接受一个error类型参数;如果写成fmt.Errorf("xxx: %w", err, "extra")会编译报错。
什么时候该用panic而不是返回error?
panic在Go中仅用于**不可恢复的编程错误**,比如索引越界、nil指针解引用、断言失败——这些本应通过测试提前发现,运行时遇到说明逻辑有严重缺陷。
常见误用场景包括:
- 打开配置文件失败 → 应返回
error,让上层决定重试/降级/退出 - HTTP handler里数据库查询失败 → 返回
500响应,不是panic - 解析用户输入JSON失败 → 返回
400,不是panic
唯一合理使用recover的地方,是HTTP服务器的顶层中间件(如http.Server内部已做recover),用于防止单个请求崩溃整个服务。自己写业务逻辑时,几乎不需要recover。
错误日志记录要分清“记录”和“传播”,避免重复打印
一个典型反模式是:在每层都log.Printf("err: %v", err),再return fmt.Errorf("xxx: %w", err)。结果同一错误被打印多次,日志爆炸,还掩盖了真正出问题的位置。
推荐做法:
- 只在**错误首次发生处**或**边界处**(如API入口、main函数)记录完整错误链:
log.Printf("request failed: %+v", err)(用%+v可展开所有包装层) - 中间层专注增强错误,不打日志;除非需要审计特定环节(如“重试第3次仍失败”),才加带上下文的日志
- 生产环境禁用
fmt.Printf类调试输出,统一走结构化日志库(如zap),并确保err字段能被正确序列化
错误链的深度和上下文丰富度,和日志的时机与位置一样重要——太多层包装会让%+v输出难以阅读,太少又丢失关键路径信息。










