错误码必须分层封装为实现error接口的结构体并集中管理。应定义CodeError结构体绑定码值、消息和HTTP状态码,通过pkg/code/统一声明const码值和var错误实例,禁止硬编码;所有错误需在handler入口统一包装转换,底层异常须在DAO层转为业务错误,确保日志可排查、前端可解析、协作低成本。

Go 项目里错误码不统一,会导致日志难排查、前端解析混乱、协作成本高——这不是设计问题,是没做错误码分层和封装。
错误码必须和 error 绑定,不能只靠字符串或整数
很多人用 fmt.Errorf("code:1001, msg:user not found") 或直接返回 1001 整数,这会让调用方无法类型断言、无法结构化提取码值,也破坏 Go 的错误处理惯性。
正确做法是定义一个实现了 error 接口的结构体,内嵌码、消息、HTTP 状态码等字段:
type CodeError struct {
Code int `json:"code"`
Message string `json:"message"`
HTTPCode int `json:"http_code,omitempty"`
}
func (e *CodeError) Error() string {
return e.Message
}
func (e *CodeError) WithHTTPCode(code int) *CodeError {
e.HTTPCode = code
return e
}
-
CodeError可被errors.As()捕获,支持类型安全判断 - 避免在
Error()方法里拼接码值(如return fmt.Sprintf("[%d] %s", e.Code, e.Message)),否则日志中会重复出现码值,干扰 grep 和 ELK 解析 - 不要把
HTTPCode默认设为 500——400 类错误(如参数校验失败)应显式设为 400,便于网关透传
错误码定义必须集中管理,且不可变
把错误码散落在各 handler 或 service 文件里,很快就会出现 1001 在 user 包表示“用户不存在”,在 order 包却表示“订单过期”。
立即学习“go语言免费学习笔记(深入)”;
推荐做法:在项目根目录建 pkg/code/,用 const + var 分组定义:
package code
const (
UserNotFound = 1001
UserAlreadyExists = 1002
)
var (
ErrUserNotFound = &CodeError{Code: UserNotFound, Message: "user not found", HTTPCode: 404}
ErrUserAlreadyExists = &CodeError{Code: UserAlreadyExists, Message: "user already exists", HTTPCode: 409}
)
- 所有业务错误码从
code包引用,禁止硬编码数字或字符串 - const 定义码值,
var定义具体 error 实例——这样既可导出码值供文档/前端使用,又可复用 error 实例(节省内存) - 新增错误码必须加单元测试,验证
errors.Is(err, code.ErrXxx)返回 true
对外返回前必须统一包装,避免底层错误泄露
数据库超时、Redis 连接失败、第三方 HTTP 错误这些底层 error 如果直接返回给前端,会暴露技术细节(比如 "dial tcp 10.0.1.2:6379: i/o timeout"),也违反错误码规范。
建议在 HTTP handler 入口或中间件做统一错误转换:
func ErrorHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
WriteError(w, code.ErrInternalError)
return
}
}()
next.ServeHTTP(w, r)
})
}
func WriteError(w http.ResponseWriter, err error) {
var codeErr *code.CodeError
if errors.As(err, &codeErr) {
w.WriteHeader(codeErr.HTTPCode)
json.NewEncoder(w).Encode(map[string]interface{}{
"code": codeErr.Code,
"message": codeErr.Message,
})
} else {
// 非规范错误,兜底转为 500
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]interface{}{
"code": code.InternalError,
"message": "internal server error",
})
}
}
- 不要在每个 handler 里写
if err != nil { return code.ErrXxx }——容易漏,也难以统一加 traceID、logID - 第三方库错误(如
redis.Nil、pgx.ErrNoRows)应在 DAO 层就转成业务错误,不要透传到 service - 日志记录时,用
log.Error("db query failed", "err", err, "trace_id", r.Context().Value("trace_id")),保留原始 error 供排查,但响应体只输出规范错误
最难的不是定义错误码,而是让所有人——包括新来的同事、临时支援的后端、甚至写脚本的 QA——都只用 code.ErrXxx,而不是自己 new 一个。这需要 CI 检查(比如禁止代码中出现裸数字 1001)、CR 时重点看 error 使用,以及一次踩坑后的全员同步。


















