必须用导出字段的结构体实现error接口并实现unwrap(),否则errors.as/is失效;code映射状态码,message为小写无标点提示,禁用字符串错误和type别名。

直接用 errors.New 或 fmt.Errorf 返回的错误,调用方根本没法区分是参数错、DB 超时还是权限不足——只能靠字符串匹配,一改文案就崩。必须用结构体实现 error 接口,并导出字段、加 Unwrap(),否则 errors.As 和 errors.Is 会失效。
定义带 Code 和 Message 的结构体错误
核心就两件事:字段首字母大写(导出)、实现 Error() 方法。别嵌 error 字段凑数,除非你真要包装底层错误。
-
Code字段用于映射 HTTP 状态码或内部错误码,比如400、5001 -
Message是面向调用方的简短提示,小写开头、不加句号,例如"invalid email format" - 避免在
Error()里拼接敏感信息(如 token、密码),日志一打就泄露 - 不要用
type MyError string这种方式——它没法带字段,后续扩展成本高
type AppError struct {
Code int
Message string
RequestID string
}
func (e *AppError) Error() string {
return e.Message
}
必须实现 Unwrap() 才能让 %w 包装生效
如果业务逻辑里要用 fmt.Errorf("xxx: %w", err) 套一层,但你的自定义错误没实现 Unwrap(),那整个错误链就断了:errors.Is 查不到原始错误,errors.As 也提取不到类型。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
-
Unwrap()必须返回一个error类型值,不能返回nil(除非你确定这层就是终点) - 常见错误是只写了
Error(),忘了加Unwrap(),结果上层怎么errors.As(err, &target)都失败 - 如果错误本身不包装其他错误,
Unwrap()可以直接返回nil;如果包装了,就返回那个Cause字段
func (e *AppError) Unwrap() error {
return e.Cause // 假设你加了 Cause 字段
}
用 errors.As 安全提取错误,别用类型断言
写 if myErr, ok := err.(*AppError) 看似简单,但一旦错误被 fmt.Errorf("wrap: %w", err) 包过一层,这个断言就失效。而 errors.As 能穿透多层包装,只要任意一层是目标类型就能命中。
- 变量必须是指针类型,比如
&target,不是target - 多个包各自定义了同名
AppError,哪怕字段完全一样,errors.As也会失败——Go 认为它们是不同类型 - 构造函数推荐用工厂函数封装,比如
NewValidationError("email", "empty"),避免外部直接 new 结构体
var target *AppError
if errors.As(err, &target) {
log.Printf("code=%d, reqid=%s", target.Code, target.RequestID)
}
别在 handler 里堆 if errors.As 判断
每个接口都写一堆 errors.As 提取不同错误类型,不仅重复,还容易漏掉新错误类型。应该把错误分类逻辑收口到统一的错误处理器里,比如按 Code 映射 HTTP 状态码,或按类型分发告警。
- 同一错误类型在多个包里重复定义,会导致
errors.As失效——必须确保所有地方引用的是同一个结构体定义 - 临时性错误(如网络超时)可选实现
Temporary() bool,方便重试逻辑识别 - 日志记录时,优先打
err.Error()+target.Code,而不是直接%+v打结构体——避免泄露敏感字段
最常被忽略的一点:错误类型的定义位置和导出一致性。一个项目里如果 AppError 在三个包里各定义了一次,哪怕字段一模一样,errors.As 就永远找不到。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










