
本文介绍一种可扩展、易维护的 go 错误组织方式:按模块定义带唯一错误码的自定义错误类型,避免全局错误常量污染,同时支持客户端识别、服务端日志追踪和多语言响应。
本文介绍一种可扩展、易维护的 go 错误组织方式:按模块定义带唯一错误码的自定义错误类型,避免全局错误常量污染,同时支持客户端识别、服务端日志追踪和多语言响应。
在构建健壮的 Go Web 服务时,返回给客户端的错误不应是模糊的字符串(如 "internal server error"),而应是结构化、可解析、可本地化的错误对象。理想响应如下:
{ "code": 5001, "message": "订单不存在", "details": "order_id=12345" }
其中 code 是全局唯一、语义明确的整型错误码,message 可根据 Accept-Language 动态翻译,details 供调试但不暴露敏感信息。
✅ 推荐方案:模块内定义、导出、封装的自定义错误类型
不要将所有错误硬编码进一个 errors.go(易冲突、难维护),也不要依赖“1000 进制”手动分段编号(违反单一职责、缺乏类型安全)。正确做法是:每个业务模块(如 clienthandler、orderhandler)自行定义并导出其专属错误类型,该类型实现 error 接口,并内嵌错误码、原始消息与上下文数据。
以下是一个生产就绪的示例:
// clienthandler.go
package server
import "fmt"
// ClientError 表示客户端相关错误,实现 error 接口
type ClientError struct {
Code int
Message string
Details string // 可选:用于日志或调试,不直接返回给前端
}
func (e *ClientError) Error() string {
if e.Details != "" {
return fmt.Sprintf("%s: %s", e.Message, e.Details)
}
return e.Message
}
// 预定义导出错误变量(推荐)
var (
ErrClientNotFound = &ClientError{
Code: 1001,
Message: "客户端未找到",
}
ErrInvalidEmail = &ClientError{
Code: 1002,
Message: "邮箱格式无效",
}
)
// 工厂函数:支持动态构造带上下文的错误(如参数名、值)
func NewInvalidParamError(param string, value interface{}) *ClientError {
return &ClientError{
Code: 1003,
Message: "参数无效",
Details: fmt.Sprintf("参数 [%s] 值 [%v] 不合法", param, value),
}
}
同理,orderhandler.go 中定义 OrderError 类型及 ErrOrderNotFound 等导出变量,彼此完全解耦。
? 在 HTTP 处理器中统一响应
在 handler 中直接使用模块导出的错误,并通过中间件或工具函数转为 JSON:
func (h *ClientHandler) GetClient(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
client, err := h.service.FindClient(id)
if err != nil {
if errors.Is(err, ErrClientNotFound) {
respondError(w, err, http.StatusNotFound) // code=1001
return
}
respondError(w, ErrInternalServerError, http.StatusInternalServerError)
return
}
respondJSON(w, client)
}
func respondError(w http.ResponseWriter, err error, statusCode int) {
// 提取错误码与消息(支持多语言)
code := getErrorCode(err)
message := getLocalizedMessage(err, r.Header.Get("Accept-Language"))
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(map[string]interface{}{
"code": code,
"message": message,
})
}
? getErrorCode() 可通过类型断言提取:if e, ok := err.(*ClientError); ok { return e.Code };getLocalizedMessage() 可对接 i18n 包(如 github.com/nicksnyder/go-i18n/v2/i18n)。
⚠️ 注意事项与最佳实践
- 错误码唯一性保障:由团队约定前缀规则(如 1xxx=Client, 2xxx=Order, 5xxx=System),而非强制数学分段。代码审查 + CI 检查(如扫描 const Err* = \d+)可防重复。
- 避免 panic 传播:所有 error 返回都应被显式处理,禁止裸 panic(err)。
- 日志需包含完整错误链:使用 fmt.Errorf("failed to process order: %w", err) 保留原始错误上下文。
- 不导出内部细节字段:Details 字段仅用于服务端日志,绝不写入响应体,防止信息泄露。
- 测试友好:因错误是具体类型,可直接 assert.IsType(t, &ClientError{}, err) 进行单元测试。
这种设计让错误真正“属于”其业务域,提升可读性、可测试性与长期可维护性——你不再需要打开一个 2000 行的 errors.go 来查找某个订单校验失败的码值,只需导入 server 包并查看 orderhandler.go 即可。











