Go中重复检查err != nil的根源是错误传播未结构化,常见于嵌套调用与资源初始化;应区分错误发生与决策,避免字符串比对错误,优先用errors.Is和自定义错误类型,慎用recover,合理使用multierr合并清理型错误。
err != nil 的典型场景和问题根源Go 里反复写 if err != nil { return err } 不是风格问题,而是错误传播路径没被结构化。最常见于嵌套调用、资

defer 清理逻辑中——每个步骤都独立判错,但实际只需要在关键出口点统一处理。
根本原因在于把“错误发生”和“错误决策”混在一起:每个函数调用后立刻判断,却没区分“这个错误是否该立刻返回”还是“可以继续尝试其他路径”。比如连接数据库失败后还去读配置文件,就属于逻辑错位。
errors.Is 替代字符串比对很多人用 err.Error() == "xxx" 或 strings.Contains(err.Error(), "timeout") 判断错误类型,这极其脆弱:一旦底层库改了错误消息,代码就 silently 失效。
正确做法是让错误携带语义,而不是文本:
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 错误)runtime error: invalid memory address 这类模糊提示error 接口设计本意就是显式传递,强行绕过会让调用链失去可控性真正该做的是把易错操作封装成返回 (T, error) 的函数,并在顶层集中处理——比如所有 DB 查询都走一个 QueryRowContext 封装,内部统一加超时和重试,外部只关心最终 error。
multierr 合并多个错误,但别滥用当必须执行多个可能失败的操作(如关闭多个文件、批量写入日志),且希望全部执行完再返回所有错误时,github.com/hashicorp/go-multierror 是合理选择。
但要注意边界:
multierr.Append,比如 f.Write(b); multierr.Append(err, f.Close()) ——这会让主错误被稀释,errors.Is 失效nil:if err != nil { errs = multierr.Append(errs, err) }
500 Internal Server Error 即可,不必把所有细节透出给客户端真正难处理的不是“怎么合并”,而是“哪些错误值得合并”——通常只有清理型操作(close、flush、shutdown)才适合批量收集,业务逻辑错误仍应尽早返回。