go自定义error核心是类型安全区分错误和结构化携带字段;需导出字段、指针接收者实现error()与unwrap(),避免字符串拼接和敏感信息泄露。

Go 里自定义 error 不是为了让日志看起来更炫,而是解决两个硬需求:调用方能靠类型安全区分 os.ErrNotExist 和业务层的 UserNotFound;错误里能带出 Code、RequestID 这类可结构化提取的字段。字符串匹配 err.Error() 一改文案就崩,线上排查直接抓瞎。
为什么 errors.As 总是返回 false
常见原因就两个:errors.As 找不到目标类型,或者你的自定义错误没实现 Unwrap()。比如中间某层用了 fmt.Errorf("failed: %v", err)(用了 %v 而不是 %w),原始错误就被转成字符串,链就断了;又或者你定义了 type AppError struct{...},但忘了加 func (e *AppError) Unwrap() error { return e.Cause },那 errors.As 穿透不了包装层,自然失败。
必须注意:
-
errors.As的第二个参数必须是指针,写成&target,不是target - 多个包各自定义了同名
*AppError,哪怕字段完全一样,Go 也认为是不同类型,errors.As必然失败 - 如果错误本身不包装其他错误,
Unwrap()可以直接返回nil;否则得返回你存的底层错误(比如e.Cause或e.Err)
如何定义一个真正能用的结构体 error
核心就三件事:字段首字母大写(导出)、指针接收者实现 Error()、返回纯可读字符串(不拼接敏感信息或堆栈)。别嵌 error 字段凑数,除非你真要包装底层错误。
典型结构体示例:
type AppError struct {
Code int
Message string
RequestID string
Cause error // 可选,用于包装
}
func (e *AppError) Error() string {
return e.Message // 就是这么简单,别加 "code=xxx" 或 request_id=...
}
func (e *AppError) Unwrap() error {
return e.Cause
}
关键点:
-
Message是给调用方看的,小写开头、无标点,例如"user not found" - 敏感字段如
Token、Password绝对不要塞进Error()返回值,日志一打就泄露 - 用指针接收者(
func (e *AppError) Error()),避免值拷贝;结构体含[]byte或大map时这点更重要
要不要加 Temporary() 或 Timeout()
只在你用的库会主动检查它们时才加——比如 net/http.Transport、重试框架(如 backoff)会根据 Temporary() bool 决定是否重试。不加也没错,但会丢掉一层语义控制。
加的话按 HTTP 状态码惯例判断就行:
-
Temporary():返回e.Code == 408 || e.Code >= 500 && e.Code -
Timeout():更窄,返回e.Code == 408 || e.Code == 429 - 这两个方法接收者类型要和
Error()保持一致,推荐统一用指针接收者 - 别为了“看起来完整”硬加,如果你的错误从不参与网络重试或超时决策,加了反而干扰阅读
构造函数和使用姿势怎么写才不翻车
别让外部直接 &AppError{...},用工厂函数封装,比如 NewValidationError("email", "empty"),既统一初始化逻辑,也方便后续加默认字段或校验。
在 handler 里别堆一堆 if errors.As(err, &e1) { ... } else if errors.As(err, &e2) { ... },容易漏新类型。建议收口到一张错误映射表,统一做 switch 或查表分发。
最容易被忽略的一点:所有字段名必须导出(首字母大写),否则外部包读不到 Code 或 RequestID;而 Error() 方法里绝不能调用 fmt.Sprintf 拼接底层错误——那是 Unwrap() 和 %w 的事,不是 Error() 的职责。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











