error接口不能直接赋值字符串,因为其定义为type error interface { Error() string },字符串字面量不满足该接口;必须通过errors.New、fmt.Errorf或自定义实现Error()方法的结构体来创建error值。

为什么 error 接口不能直接赋值字符串就完事?
因为 Go 的 error 是一个接口:type error interface { Error() string }。你用 errors.New("xxx") 或 fmt.Errorf("xxx") 得到的值,背后都是实现了这个接口的结构体,不是原始字符串。直接写 return "xxx" 会编译报错:cannot use string literal (type string) as type error。
自定义错误类型的核心,就是让自己的 struct 实现 Error() 方法 —— 不是重命名、不是加字段,而是必须满足这个签名。
- 方法名必须是
Error,首字母大写,返回string - 接收者建议用指针(尤其当结构体含字段时),避免拷贝和 nil 指针 panic
- 不要在
Error()里做 I/O、锁或耗时操作 —— 日志打印、panic 恢复等场景会频繁调用它
怎么带上下文信息?比如错误码、时间、请求 ID?
标准库的 fmt.Errorf 支持格式化,但没法存结构化数据。要携带额外字段(如 Code、RequestID),就得自己定义 struct:
type MyError struct {
Code int
Message string
RequestID string
Timestamp time.Time
}
func (e *MyError) Error() string {
return fmt.Sprintf("[%d] %s (req=%s)", e.Code, e.Message, e.RequestID)
}
注意:如果只读取 e.Message,Timestamp 字段就纯属冗余;但如果后续要做错误分类(比如按 Code 做 switch)、日志结构化输出或 HTTP 响应映射,这些字段就立刻有用。
立即学习“go语言免费学习笔记(深入)”;
- 别把敏感信息(如密码、token)塞进
Error()返回值 —— 它可能被任意日志系统打印出来 - 如果字段可能为 nil(比如
*string),Error()里要判空,否则 panic - 想兼容
errors.Is/errors.As?得让类型可比较,或实现Unwrap()方法(见下一点)
如何链式包装错误并保留原始类型?
Go 1.13 引入了错误包装机制。用 fmt.Errorf("failed: %w", err) 包装后,原始错误没丢,还能用 errors.Unwrap(err) 取出,或用 errors.Is(err, targetErr) 判断是否包含某个底层错误。
如果你的自定义错误也要支持包装,只需在 Error() 外再加一个 Unwrap() 方法:
func (e *MyError) Unwrap() error {
return e.Cause // 假设你加了一个 Cause 字段
}
这样下游就能用 errors.Is(err, ErrNotFound) 精准判断,而不是靠字符串匹配。
-
%w只能用一次,且必须是最后一个动词 ——fmt.Errorf("x: %w, y: %v", err, val)是非法语法 - 不要在
Unwrap()里返回新错误实例(比如fmt.Errorf(...)),会导致无限递归或内存泄漏 - 如果不想被 unwrap(比如某些中间件错误需拦截),就不实现
Unwrap(),或者让它返回nil
什么时候该用自定义错误,而不是 fmt.Errorf?
不是所有错误都需要自定义类型。只有当你需要以下任一能力时,才值得多写几行:
- 下游代码要根据错误类型做不同处理(比如重试逻辑只对
*TimeoutError生效) - 错误要携带非字符串元数据(HTTP 状态码、数据库错误号、重试次数)
- 要支持结构化日志(JSON 输出里带
"code": 400, "req_id": "abc123") - 团队约定错误必须可分类、可监控、不可丢失上下文
否则,fmt.Errorf("failed to parse config: %w", err) 足够清晰、轻量、符合 Go 习惯。滥用自定义错误只会增加类型断言、接口转换和维护成本。
最常被忽略的一点:自定义错误类型一旦导出,就构成 API 的一部分 —— 字段名、方法签名、是否实现 Unwrap,都会影响调用方代码的稳定性。改之前先想清楚,它是不是真得长期存在。


















