Go错误处理应立即return而非嵌套,用errors.Is/As判断包装错误,统一错误包装和日志,避免defer掩盖返回错误。

用 if err != nil 后立刻 return,别缩进再处理
Go 的错误处理天然容易嵌套,因为习惯性把 if err != nil 块写成大括号包裹的逻辑块,里面再调用其他函数——结果一层套一层。真正该做的,是让错误路径“提前退出”,主流程保持左对齐。
常见错误现象:写成这样:
if err := doA(); err != nil {
log.Println(err)
if err := doB(); err != nil {
log.Println(err)
if err := doC(); err != nil {
return err
}
}
}
这已经掉进嵌套地狱了。正确做法是每个错误都单独判断、立即返回:
- 每个
err检查后紧跟return或panic(视场景),不加 else 块 - 把成功路径的代码放在顶层缩进,视觉上就是一条直线向下
- 如果需要清理资源,用
defer配合匿名函数,而不是靠缩进组织“失败后回滚”逻辑
用 errors.Is 和 errors.As 替代 == 判断错误类型
扁平化不只是缩进问题,更是错误传播链被破坏后的可维护性问题。直接用 == 比较错误值,会漏掉包装错误(比如 fmt.Errorf("failed: %w", err)),导致上游错误处理失效,被迫加更多嵌套来兜底。
立即学习“go语言免费学习笔记(深入)”;
使用场景:你想在 handler 里区分是数据库连接失败还是记录不存在,但下游函数返回的是包装过的错误。
-
errors.Is(err, sql.ErrNoRows)能穿透多层%w包装找到原始错误 -
errors.As(err, &target)可以提取底层具体错误类型,用于调用其方法(比如target.Code()) - 避免写
strings.Contains(err.Error(), "no rows")—— 这种字符串匹配脆弱且无法跨包复用
把重复的错误转换和日志收口到中间函数或封装类型里
每个函数都自己写 log.Printf("failed to %s: %v", op, err) + return fmt.Errorf("xxx: %w", err),既冗余又难统一行为。一旦要加 trace ID、采样日志、或切换错误上报方式,就得改几十个地方。
参数差异:不是所有错误都需要打日志;有些要告警,有些只 debug 级别;有些错误应该抹掉细节再返回给调用方。
- 定义一个
wrapErr(op string, err error) error函数,内部统一加前缀、加字段、决定是否记录 - 对关键业务错误,定义自定义类型(如
type NotFoundError struct{ ID string }),实现Error()和Unwrap(),方便errors.Is识别 - 避免在每个
if err != nil分支里重复写日志——日志应该靠近错误发生点,或统一由包装函数处理
小心 defer 在错误路径中掩盖真实错误
扁平化之后,defer 使用频率上升(比如关文件、解锁、恢复 panic),但它可能偷偷覆盖 return 的错误值——这是最容易被忽略的坑。
性能影响:不是性能问题,而是语义错误。比如函数里先返回 os.ErrPermission,但 defer 里 f.Close() 又返回 io.ErrClosed,最终调用方拿到的是后者,完全失真。
- 在有返回值的函数中,不要让
defer调用可能失败的函数(如Close())且不检查其错误 - 如果必须 defer
Close(),用if err := f.Close(); err != nil && retErr == nil判断是否覆盖主错误 - 更稳妥的做法:把资源清理逻辑拆到独立函数,按需调用,不依赖 defer 隐式执行
复杂点在于,错误扁平化之后,控制流看似线性,但 defer 的执行时机和错误覆盖关系反而更隐蔽了。得盯着每个 return 前后有没有 defer 在悄悄改 err。










