2、标准库 errors:errors.New 与 fmt.Errorf 的基本用法,以及它们的局限性
在 Go 语言中,错误处理是程序健壮性的基石。标准库 errors 和 fmt 提供了最基础的错误构造方式:errors.New 和 fmt.Errorf。理解它们的用法与边界,是掌握错误码断言与领域错误设计的前提。
2.1 errors.New:最简单的错误哨兵
errors.New 接收一个字符串,返回一个包含该字符串的 error 接口值。每次调用都会分配一个新的错误实例,即使字符串内容完全相同。
package main
import "errors"
var ErrNotFound = errors.New("record not found")
var ErrPermissionDenied = errors.New("permission denied")
func main() {
if err := doSomething(); err != nil {
if err == ErrNotFound {
// 处理“未找到”逻辑
}
}
}
func doSomething() error {
return ErrNotFound
}
最佳实践:使用
errors.New 定义的错误通常作为包级别的导出变量(哨兵错误),供调用方通过 == 直接比较。这是 Go 错误断言的最原始形态。
2.2 fmt.Errorf:带格式化上下文的错误
fmt.Errorf 在 errors.New 的基础上增加了格式化能力,允许将动态值嵌入错误消息。它返回的仍然是普通的 error 接口值,底层同样是一个不可比较的字符串包装。
package main
import "fmt"
func openFile(path string) error {
return fmt.Errorf("cannot open file %s: permission denied", path)
}
func main() {
err := openFile("/etc/config.yaml")
fmt.Println(err)
// 输出: cannot open file /etc/config.yaml: permission denied
}
| 特性 | errors.New | fmt.Errorf |
|---|---|---|
| 参数 | 静态字符串 | 格式化字符串 + 可变参数 |
| 动态内容 | 不支持 | 支持(%s, %d, %v 等) |
| 错误比较 | 支持 == 比较(哨兵模式) | 不推荐 == 比较(含动态内容) |
| 包装能力 | 无 | Go 1.13 前无;1.13 后支持 %w |
| 适用场景 | 固定语义的哨兵错误 | 带上下文信息的临时错误 |
2.3 核心局限性:无法结构化断言
无论是 errors.New 还是 fmt.Errorf,它们返回的错误本质上都是一个不透明的字符串。调用方只能通过以下两种方式处理:
- 字符串匹配:使用
strings.Contains(err.Error(), "permission")—— 脆弱、依赖语言环境、无法应对重构。 - 哨兵比较:使用
err == ErrNotFound—— 只能比较包级别的固定变量,无法区分同一错误类型下的不同实例。
关键局限:标准库错误没有携带“错误码”、“错误类别”、“领域类型”等结构化信息。当系统需要根据错误类型执行不同恢复策略时,字符串比较和哨兵比较都会导致代码耦合度高、可维护性差。
2.4 局限性带来的实际问题
以下场景中,标准库错误的局限性会直接引发架构问题:
- 多层调用链:底层返回
fmt.Errorf("db timeout"),上层无法判断是“网络超时”还是“查询超时”,只能解析字符串。 - 国际化/多语言:错误消息是英文,前端需要展示中文时,必须做字符串映射,维护成本高。
- 错误分类:业务需要区分“客户端错误”(4xx)和“服务端错误”(5xx),但标准库错误没有类别字段。
- 测试断言:单元测试中,
err.Error()的微小改动(如大小写、标点)会导致测试失败。
2.5 何时仍可使用标准库 errors
尽管存在局限性,标准库错误在以下场景中仍然是合理的选择:
- 内部工具函数:错误仅用于日志记录,不需要调用方做断言。
- 原型开发:快速验证逻辑,后续再引入结构化错误。
- 简单哨兵:错误语义固定且全局唯一,如
io.EOF。
架构决策点:当你的系统出现以下信号时,就应该考虑超越标准库 errors:
• 代码中出现 3 处以上
• 测试中频繁因为错误消息格式变更而失败
• 需要根据错误类型执行不同的重试、降级或告警策略
• 代码中出现 3 处以上
strings.Contains(err.Error(), ...)• 测试中频繁因为错误消息格式变更而失败
• 需要根据错误类型执行不同的重试、降级或告警策略