go 的 Error 类型是接口,nil 表示成功,非 nil 表示失败;应避免用 panic 处理可预期错误,仅在不可恢复状态时使用;api 必须返回 error,错误需用 %w 包装、errors.is 判断、errors.as 提取,必要时定义结构化自定义 error 类型。

Go 的 error 类型不是异常,别用 panic 替代错误处理
Go 没有 try/catch,error 是一个接口类型,值为 nil 表示成功,非 nil 表示失败。强行用 panic 处理可预期的错误(比如文件不存在、网络超时),会让调用方无法合理恢复,也破坏了 Go “显式错误传递”的设计哲学。
常见错误现象:panic: open config.yaml: no such file or Directory —— 这本该返回 os.Open 的 error,却被包裹在 MustXXX 工具函数里提前 panic 了。
- 仅在真正不可恢复的程序状态(如初始化失败、空指针解引用)时使用
panic - 对外暴露的 API 函数必须返回
error,而不是内部panic - 避免在库代码中使用
log.Fatal或os.Exit,把退出决策权留给主程序
如何正确包装和区分错误:用 errors.Join、fmt.Errorf %w 和 errors.Is
Go 1.13 引入了错误链(error wrapping),核心是 %w 动词和 errors.Unwrap/errors.Is/errors.As。它解决了“原始错误信息丢失”和“无法判断错误类型”的老问题。
错误包装示例:
立即学习“go语言免费学习笔记(深入)”;
if err != nil { return fmt.Errorf("failed to read user %d: %w", userID, err) }
后续可这样判断或提取:
- 用
errors.Is(err, os.ErrNotExist)判断是否是文件不存在(无视中间包装层) - 用
errors.As(err, &pathErr)提取底层*os.PathError - 用
errors.Join(err1, err2)合并多个独立错误(如并发任务中多个子错误) - 避免只写
fmt.Errorf("xxx: %v", err)—— 这会切断错误链,%w才能保留原始错误
何时该返回自定义 error 类型,而不是字符串
当错误需要携带结构化信息(如错误码、重试建议、http 状态码)、或需被其他模块精确识别和处理时,应定义实现 error 接口的结构体,而非简单返回 fmt.Errorf 字符串。
典型场景:
- API 网关需根据错误类型返回不同 HTTP 状态码(
400 Bad Requestvs503 Service Unavailable) - 客户端需识别是否为临时性错误并自动重试(如
TimeoutError实现Temporary() bool方法) - 日志系统需提取错误字段做聚合分析(如
Code String、TraceID string)
示例:
type ValidationError struct { Code string Field string Message string } func (e *ValidationError) Error() string { return e.Message } func (e *ValidationError) Temporary() bool { return false }
defer + recover 不是 Go 错误处理机制的一部分
defer 和 recover 仅用于捕获 panic,不能替代常规错误处理流程。滥用它们会导致控制流混乱、资源泄漏风险升高,且违背 Go 的显式错误风格。
真实限制:
-
recover只在defer函数中有效,且仅对同 goroutine 的 panic 生效 - 无法从
recover中拿到原始错误类型(只能拿到Interface{}),失去类型安全 - 一旦用了
recover,就很难保证 defer 链中其他清理逻辑(如 unlock、close)仍被执行 - 绝大多数服务程序不应在业务逻辑里写
recover;它只适合在顶层 goroutine(如 HTTP handler)兜底防止崩溃
最易被忽略的一点:即使你写了 recover,如果 panic 发生在另一个 goroutine,它根本捕获不到——而这种并发 panic 往往才是线上事故的根源。