Go中重复检查err != nil的根源是错误传播未结构化,常见于嵌套调用与资源初始化;应区分错误发生与决策,避免字符串比对错误,优先用errors.Is和自定义错误类型,慎用recover,合理使用multierr合并清理型错误。

重复检查 err != nil 的典型场景和问题根源
Go 里反复写 if err != nil { return err } 不是风格问题,而是错误传播路径没被结构化。最常见于嵌套调用、资源初始化(如打开文件 + 解析 JSON + 关闭)、或多个 defer 清理逻辑中——每个步骤都独立判错,但实际只需要在关键出口点统一处理。
根本原因在于把“错误发生”和“错误决策”混在一起:每个函数调用后立刻判断,却没区分“这个错误是否该立刻返回”还是“可以继续尝试其他路径”。比如连接数据库失败后还去读配置文件,就属于逻辑错位。
用自定义错误类型 + errors.Is 替代字符串比对
很多人用 err.Error() == "xxx" 或 strings.Contains(err.Error(), "timeout") 判断错误类型,这极其脆弱:一旦底层库改了错误消息,代码就 silently 失效。
正确做法是让错误携带语义,而不是文本:
立即学习“go语言免费学习笔记(深入)”;
var ErrTimeout = errors.New("operation timeout")
func DoWork() error {
if timedOut {
return fmt.Errorf("%w: context deadline exceeded", ErrTimeout)
}
return nil
}
// 调用方
if errors.Is(err, ErrTimeout) {
// 重试或降级
}
-
errors.Is检查错误链中任意一层是否为指定错误,不依赖字符串 - 避免用
errors.As做类型断言来取值,除非你真需要访问错误内部字段 - 第三方库返回的错误(如
os.PathError)可直接用errors.Is(err, fs.ErrNotExist),无需自己包装
组合多个操作时,用 defer func() + recover 不是好主意
有人想“统一捕获 panic 再转成 error”,比如在 HTTP handler 里 defer recover 并返回 500。这看似减少判错,实则掩盖真正问题:
-
panic是异常控制流,不该用于常规错误(如参数校验失败、I/O 错误) - recover 后无法还原栈信息,调试时只剩
runtime error: invalid memory address这类模糊提示 - Go 的
error接口设计本意就是显式传递,强行绕过会让调用链失去可控性
真正该做的是把易错操作封装成返回 (T, error) 的函数,并在顶层集中处理——比如所有 DB 查询都走一个 QueryRowContext 封装,内部统一加超时和重试,外部只关心最终 error。
用 multierr 合并多个错误,但别滥用
当必须执行多个可能失败的操作(如关闭多个文件、批量写入日志),且希望全部执行完再返回所有错误时,github.com/hashicorp/go-multierror 是合理选择。
但要注意边界:
- 不要在单个 I/O 操作后就用
multierr.Append,比如f.Write(b); multierr.Append(err, f.Close())——这会让主错误被稀释,errors.Is失效 - 合并前先判断是否为
nil:if err != nil { errs = multierr.Append(errs, err) } - HTTP handler 中若合并了 5 个错误,返回
500 Internal Server Error即可,不必把所有细节透出给客户端
真正难处理的不是“怎么合并”,而是“哪些错误值得合并”——通常只有清理型操作(close、flush、shutdown)才适合批量收集,业务逻辑错误仍应尽早返回。











