不能只用 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 statu
s, 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) —— 这会丢掉错误码和状态码。必须用自定义构造函数做“语义升格”。
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 获取)Message 已足够清晰,额外描述应放在日志里,而非错误值中在 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,兼容标准库错误,避免过度约束c.AbortWithStatusJSON 的 wrapper,但核心逻辑不变fmt.Errorf 一把梭。一旦放开这个口子,三个月后你就会在三个不同文件里看到几乎一样的 "order not found" 字符串。