go错误处理应优先直接return err而非包装,以保留原始错误类型和调用栈;需正确实现unwrap支持errors.is/as;多错误合并用errors.join而非字符串拼接。

用 if err != nil 之后立刻 return,别包一层再 return
Go 的错误处理不是靠 try/catch,而是靠显式检查和提前退出。很多人写成这样:
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
看起来“更规范”,但实际掩盖了原始错误类型,也打断了错误路径的可追溯性。除非你真在做错误分类或加上下文,否则直接 return err 更干净、更快、更容易调试。
- 原始错误带调用栈(比如
os.Open返回的*os.PathError)能被errors.Is或errors.As正确识别 - 多层包装会让
%+v输出冗长,日志里难定位真正出错点 - 如果函数签名返回的是
error,就别硬塞成fmt.Errorf—— 除非你要隐藏敏感信息或统一错误码
把重复的 if err != nil 抽成闭包或辅助函数,但别滥用
连续几行 os.Open、os.Stat、json.Unmarshal 后都跟同一段错误处理?可以封装,但要注意边界。
- 适合封装的场景:固定流程(如“打开 → 解析 → 关闭”),且错误处理逻辑完全一致(比如全是
return err) - 不适合封装的场景:不同步骤需要不同错误处理(比如文件不存在可忽略,但解码失败必须告警)
- 常见坑:传入闭包时捕获了变量地址而非值,导致所有迭代共享同一个
err;正确做法是把err作为闭包参数显式传入
简单示例(安全):
立即学习“go语言免费学习笔记(深入)”;
check := func(err error) error {
if err != nil {
return err
}
return nil
}
f, err := os.Open("config.json")
if err := check(err); err != nil {
return err
}
用 errors.Join 合并多个错误,但注意它不等于“聚合日志”
当一个操作可能触发多个独立错误(比如并发关闭多个资源),errors.Join 是 Go 1.20+ 提供的正解。但它不是为了拼接字符串,而是为了保留每个错误的完整结构。
-
errors.Join(err1, err2)返回的仍是error类型,支持errors.Is和errors.As对任意子错误进行判断 - 不要用
fmt.Sprintf拼接错误信息来“模拟合并”——那样会丢失类型和原始堆栈 - 注意:如果任一参数是
nil,errors.Join会自动跳过,所以不用提前判空
自定义错误类型时,优先实现 Unwrap 而非只加字段
如果你写了 type MyError struct { Msg string; Code int },那它几乎无法参与标准错误链判断。真正有用的自定义错误,得让 errors.Is 和 errors.As 能穿透。
- 必须实现
Unwrap() error方法,返回底层错误(通常是包装的err字段) - 如果要支持类型断言,还得让结构体字段可导出,并在
Unwrap中返回原始错误 - 常见错误:只实现了
Error() string,结果errors.Is(err, fs.ErrNotExist)永远 false
最小可用模板:
type MyError struct {
err error
code int
}
func (e *MyError) Error() string { return e.err.Error() }
func (e *MyError) Unwrap() error { return e.err }
func (e *MyError) Code() int { return e.code }
最麻烦的地方往往不在写错,而在“以为自己写对了”——比如加了 fmt.Errorf 就觉得有上下文了,或者实现了 Error() 就以为能被 Is 判断。错误类型的可组合性,得从第一行 Unwrap 开始验证。










