与 Java、C++、JavaScript 和 Python 等语言相比,Go 对错误采用了相对不同的理念:在 Go 中,错误是内置类型 error 的值,函数通常会将其与其他返回值一起返回给调用者。这样一来,检查和处理错误就成为程序执行路径中明确可见的一部分,而不是交由独立的异常机制处理。
JetBrains 指南最初由社区贡献者 Christoph Berger 撰写,随后转载至 JetBrains Go 博客,介绍了一系列处理输入输出、网络、数据验证等错误的技术和实践。该指南于 2026 年 8 月更新,以跟进 Go 语言的最新变化。
返回是起点
当函数没有足够的上下文来处理问题时,就应将错误返回给调用它的函数。Go 通常将错误值放在返回列表的最后;例如 ReadFile() 这样的函数会返回文件内容和一个错误,成功时错误值为 nil,失败时则为其他值。
指南指出,调用者应立即检查错误,而不是忽略它。如果函数打开了文件或网络连接等资源,就应使用 defer 在退出时进行清理,但必须先确认打开操作成功,因为尝试关闭无效资源可能会引发额外问题。
添加上下文而不破坏错误结构
错误在被处理或记录之前,可能会经过一连串函数。在此过程中,每个函数都可以添加有用的信息,例如失败的操作或正在处理的路径。但通过拼接 err.Error() 将错误转换为文本,会丢失错误链和错误的类型结构。
正确做法是将 fmt.Errorf() 与 %w 操作符结合使用,在保留原始错误的同时对其进行包装。之后可以使用 errors.Unwrap() 访问一层错误,也可以使用 errors.Is() 和 errors.As() 检查包装链中的特定错误。Go 1.26 添加了通用函数 errors.AsType(),作为 As() 的类型安全替代方案;它会返回匹配的错误和一个布尔值,并允许编译器发现旧方法中可能在运行时出现的类型错误。
多个错误与已取消的上下文
错误并不总是呈线性链式结构。例如,在处理一组文件时,某些操作可能成功,而其他操作可能失败。标准库提供了自 Go 1.20 起可用的 errors.Join(),用于将多个错误汇总为一个值,同时保留已成功处理的内容。
指南提醒,errors.Unwrap() 只返回一个值,因此处理合并错误时会返回 nil。要访问汇总的错误,必须检查该值是否实现了提供 Unwrap() []error 的接口。此外,自 Go 1.20 起,还可以使用 context.WithCancelCause() 将取消上下文与自定义原因关联起来,然后通过 context.Cause(ctx) 获取该原因,而不只是使用通用值 context.Canceled。
何时适合使用 panic?
panic 和 recover 不应成为检查错误的常规替代方案。用户输入无效、文件缺失或网络超时等可预期错误,应通过常规返回路径进行处理和返回。
当问题不可预期且没有有意义的处理方式时,使用 panic 才较为合适,例如编译一个本应提前验证有效性的固定正则表达式失败。在 HTTP 服务器中,应用可以在延迟函数中使用 recover(),防止当前请求的处理崩溃影响其他请求,只要情况允许这样做。至于内存耗尽等情况,应用可能没有实际可行的继续运行选项。
对开发者而言,实践中哪些方面最重要?
- 不要忽略错误:将错误值赋给空白标识符,或丢弃唯一的返回值,可能会延迟问题的发现,并使后续影响更难诊断。
- 在合适的位置记录:函数应处理错误,或将其返回给调用者。指南建议库不要自行记录日志,因为其使用者可能偏好不同的日志格式或输出目标。
- 使用合适的类型:自定义错误类型可以携带额外信息,正如 fs.PathError 所做的那样,它提供操作、路径和内部错误。
- 区分网络错误:net.OpError 可以检查连接失败的性质,而临时错误可能允许重试,或使用指数退避等策略。
- 利用输入输出操作中的字节数:io.Reader 接口会返回已处理的字节数以及错误,这可能有助于恢复操作,而不必重新处理全部数据。
- 注意 io.EOF:按照该包的语义,这个值表示读取流已成功结束,并不一定是需要作为普通错误记录的失败。
- 谨慎使用 log.Fatal():它会调用 os.Exit(),从而跳过延迟函数。因此,指南建议限制其使用,或在 main() 函数中使用 os.Exit()。
certi.news 的编辑结论是,清晰的错误路径是程序设计的一部分,而不仅仅是一种冗长的写法。结构化的包装、合适的类型和负责任的日志记录能让诊断真正可执行;而依赖 panic、笼统的消息或丢弃错误,则会隐藏开发者之后所需的信息。实际取舍仍取决于应用场景:请求服务器可能需要有限的恢复机制,而无法恢复的错误则可能需要终止并重启。