应使用嵌入error字段并实现Unwrap()的自定义结构体替代errors.New和fmt.Errorf,每个模块定义专属错误类型(如UserError),提供ErrorCode()、HTTPStatus()、ErrorMessage()方法,通过工厂函数创建错误,中间件统一处理并结构化响应。

用自定义结构体替代 errors.New 和 fmt.Errorf
直接用 errors.New("user not found") 或 fmt.Errorf("failed to get user: %v", err) 会丢失错误码、HTTP 状态码、上下文字段等关键信息,前端无法解析,日志难聚合,监控也抓不到语义。这类错误一旦被多层包装,errors.Is() 和 errors.As() 就会失效。
- 每个业务模块(如
user/、order/)应定义自己的错误类型,比如UserError,而非共享一个全局errors.go - 结构体必须嵌入
error字段并实现Unwrap()方法,否则错误链断裂,errors.Is(err, sql.ErrNoRows)永远为 false -
Error()方法里不要拼接原始错误的.Error()字符串,而要用%w包装:正确写法是return fmt.Errorf("get user: %w", origErr) - 错误码字段推荐用
int类型(如Code int),别用字符串——整数更易比较、索引,也避免拼写错误
让错误携带可提取的元数据方法
中间件和 HTTP handler 需要从任意层级的错误中稳定读取错误码、状态码、消息,不能靠反射或字符串匹配。这意味着每个自定义错误类型必须提供一组固定签名的方法。
- 必须有
ErrorCode() int方法,供统一响应中间件提取并写入 JSON 的errCode字段 - 推荐提供
HTTPStatus() int,但不要硬编码在实例里——应由错误码查表映射(如 404 类错误统一映射到 404,500 类映射到 500) -
ErrorMessage() string应返回面向客户端的提示,而非开发用 debug 信息;敏感字段(如路径、SQL 语句)必须过滤 - 避免在
Unwrap()里加逻辑判断(如if e.Code == 404 { return e.Err }),这会破坏标准库递归展开行为
禁用裸 fmt.Errorf,只暴露工厂函数
放任开发者直接调用 fmt.Errorf 是统一错误体系崩溃的起点。哪怕只有一处用了 fmt.Errorf("xxx: %v", err),整个错误链就断了,错误码再也取不出来。
- 所有模块内错误创建必须走封装好的工厂函数,例如
user.NewErrNotFound()或user.NewErrInvalidEmail(email) - 工厂函数内部完成错误码分配、默认消息设置、状态码查表,调用者无需关心细节
- 禁止导出任何裸的
errors.New或fmt.Errorf调用;可考虑在go:build标签下加 lint 规则拦截 - 若需包装底层错误(如 DB 层
sql.ErrNoRows),工厂函数应接受cause error参数,并用%w正确传递
HTTP 中间件统一拦截并结构化响应
handler 里不能裸调 http.Error(w, err.Error(), http.StatusInternalServerError),那等于把 os.PathError 或数据库连接失败直接暴露给前端。
立即学习“go语言免费学习笔记(深入)”;
- 写一个中间件,在
next.ServeHTTP后捕获error,然后用errors.As()尝试断言为*UserError、*OrderError等具体类型 - 成功断言后,取
e.ErrorCode()和e.HTTPStatus()构造 JSON 响应体:{"errCode": 1001,"errorString":"user not found","details":{}} - 断言失败时(即遇到裸
error),一律转为通用服务端错误,HTTP 状态码设为 500,errCode设为 9999,避免泄露技术细节 - 注意:中间件里别用
fmt.Sprintf("%+v", err)打日志——生产环境可能泄露路径、变量名甚至内存地址
最常被忽略的一点是错误码的“不可变性”:一旦上线,ErrUserNotFound = 1001 就永远不能改含义、不能复用、不能删。哪怕业务逻辑重构,这个码也得代表“用户不存在”这个唯一语义。否则前端翻译表、重试策略、监控告警规则全都会错乱。


















