不能只用 errors.New 或 fmt.Errorf,因其导致错误信息分散、无法区分业务/系统错误、下游难判断类型或映射错误码、日志缺上下文;应定义全局错误变量并使用自定义 Error 类型统一管理。

为什么不能只用 errors.New 或 fmt.Errorf
直接用 errors.New("user not found") 或 fmt.Errorf("failed to parse config: %w", err) 看似简单,但会导致几个实际问题:错误信息散落在各处、无法区分业务错误和系统错误、下游难以做类型判断或错误码映射、日志中缺少上下文字段(如请求 ID、用户 ID)。尤其当项目接入监控或需要国际化时,这种写法会让错误处理迅速失控。
定义全局错误变量 + 自定义错误类型
推荐在 pkg/errors 或 internal/errors 包中集中声明错误变量,并搭配自定义结构体承载错误码、HTTP 状态码、可序列化字段。关键不是“造轮子”,而是让错误具备可识别性与可扩展性。
package errors
import "fmt"
type Code int
const (
ErrUserNotFound Code = 1001
ErrInvalidToken Code = 1002
ErrDatabase Code = 5001
)
type Error struct {
Code Code
Message string
Status int // HTTP status, e.g. 404, 401, 500
}
func (e *Error) Error() string {
return e.Message
}
func (e *Error) ErrorCode() Code {
return e.Code
}
var (
UserNotFound = &Error{Code: ErrUserNotFound, Message: "user not found", Status: 404}
InvalidToken = &Error{Code: ErrInvalidToken, Message: "invalid auth token", Status: 401}
)
- 所有业务错误都从这里导出变量,避免拼写错误和重复定义
-
*Error类型可被errors.Is和errors.As正确识别,支持错误链嵌套 - 如果需要携带额外字段(如
UserID),可在结构体中添加并实现Unwrap()方法
如何包装底层错误而不丢失业务语义
调用数据库、HTTP 客户端等可能返回原始错误时,不能简单用 fmt.Errorf("%w", err) —— 这会丢掉错误码和状态码。必须用自定义构造函数做“语义升格”。
v4.5更新说明:修改店铺自定义分类为一级重新整合bbsxp论坛,修正了一致的所有错误。如分页,搜索,通行密码,选项等错误修改添加会员认证功能。认证后可以再次升级认证.增加虚拟币使用功能。可使用虚拟币购买收费店铺时间,站长可以在后台控制价格。订单管理中添加付款连接,使买家下订单后可以选择是否马上付款。增加首页两侧广告条增加在后台可以更改9大主题的名称增加修改后台的求购管理增加会员申请收费店铺及收费
func GetUserByID(id int) (*User, error) {
u, err := db.FindUser(id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, errors.UserNotFound // 直接返回预定义变量
}
return nil, &errors.Error{
Code: errors.ErrDatabase,
Message: fmt.Sprintf("failed to get user %d from db: %v", id, err),
Status: 500,
}
}
return u, nil
}
- 优先匹配已知底层错误(如
sql.ErrNoRows),映射为明确的业务错误变量 - 对未知错误,用
&Error{...}包装,保留原始错误作为 cause(可被errors.Unwrap获取) - 避免在包装时重复写 “failed to …” —— 预定义变量的
Message已足够清晰,额外描述应放在日志里,而非错误值中
HTTP handler 中如何统一响应错误
在 handler 层,不要每个地方都写 if err != nil { w.WriteHeader(404); json.NewEncoder(w).Encode(...)} 。用中间件或封装的响应函数统一处理。
立即学习“go语言免费学习笔记(深入)”;
func JSONError(w http.ResponseWriter, err error, statusCode int) {
var appErr *errors.Error
if errors.As(err, &appErr) {
statusCode = appErr.Status
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": map[string]interface{}{
"code": appErr.Code,
"message": appErr.Message,
},
})
}
// 使用示例
func userHandler(w http.ResponseWriter, r *http.Request) {
u, err := GetUserByID(123)
if err != nil {
JSONError(w, err, 0) // 0 表示由 JSONError 内部决定状态码
return
}
json.NewEncoder(w).Encode(u)
}
-
JSONError通过errors.As提取自定义错误字段,其他错误(如 panic 捕获或 net/http 超时)走默认状态码 - 不强制要求所有错误都必须是
*errors.Error,兼容标准库错误,避免过度约束 - 如果项目用 Gin/Echo,可进一步封装成
c.AbortWithStatusJSON的 wrapper,但核心逻辑不变
fmt.Errorf 一把梭。一旦放开这个口子,三个月后你就会在三个不同文件里看到几乎一样的 "order not found" 字符串。









