6.1 核心名词
- ValidationErrors
- validator 校验失败返回的错误类型,是
FieldError的切片,可逐字段拿到失败的规则名、字段名、参数。 - 翻译器(Translator)
- universal-translator + validator 翻译组件,把英文错误(如 "failed on 'email' tag")转成中文可读文案。
- 业务错误码(Error Code)
- 项目自定义的数字/字符串码,用来区分具体业务失败原因,独立于 HTTP 状态码。
- 哨兵错误(Sentinel Error)
- 预定义的 error 变量(如
ErrUserNotFound),用errors.Is判等,便于分层传递。 - 错误包装(Wrapping)
- 用
fmt.Errorf("...: %w", err)保留原始错误链,配合errors.Is/As追溯根因。 - c.Error()
- Gin 把错误挂到 Context 的错误列表(
c.Errors),由统一错误中间件在链尾集中处理。 - panic / recover
- Go 的异常机制,Recovery 中间件用
recover()兜住未预期 panic,避免整个服务崩溃。
6.2 把校验错误翻译成中文
import (
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
zhTrans "github.com/go-playground/validator/v10/translations/zh"
)
var trans ut.Translator
func initTrans() {
v, _ := binding.Validator.Engine().(*validator.Validate)
zhLoc := zh.New()
uni := ut.New(zhLoc, zhLoc)
trans, _ = uni.GetTranslator("zh")
_ = zhTrans.RegisterDefaultTranslations(v, trans)
}
// 把 ValidationErrors 转成 map[字段]中文
func transErr(err error) map[string]string {
var ve validator.ValidationErrors
if errors.As(err, &ve) {
out := map[string]string{}
for _, fe := range ve {
out[fe.Field()] = fe.Translate(trans)
}
return out
}
return map[string]string{"_": err.Error()}
}
字段名也能中文
注册 RegisterTagNameFunc 读取 label tag,可让报错里的字段名变成「邮箱」而非「Email」。例如 Email string `binding:"required,email" label:"邮箱"`。
6.3 业务错误码体系
// pkg/errcode/code.go
package errcode
type Error struct {
Code int
Msg string
HTTP int // 对应的 HTTP 状态码
}
func (e *Error) Error() string { return e.Msg }
var (
Success = &Error{0, "success", 200}
ErrParam = &Error{10001, "参数错误", 400}
ErrUnauthorized = &Error{10002, "未登录或登录已过期", 401}
ErrUserNotFound = &Error{20001, "用户不存在", 404}
ErrInternal = &Error{50000, "服务器内部错误", 500}
)
6.4 统一错误处理中间件
让 handler 只管把错误 c.Error(err) 交出去,由中间件在链尾统一渲染:
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next() // 先执行业务,再收错
if len(c.Errors) == 0 {
return
}
err := c.Errors.Last().Err
// 业务错误
var be *errcode.Error
if errors.As(err, &be) {
c.JSON(be.HTTP, gin.H{"code": be.Code, "message": be.Msg})
return
}
// 校验错误
var ve validator.ValidationErrors
if errors.As(err, &ve) {
c.JSON(400, gin.H{"code": 10001, "message": "参数错误", "fields": transErr(ve)})
return
}
// 兜底
c.JSON(500, gin.H{"code": 50000, "message": "服务器内部错误"})
}
}
// handler 极简:出错就 c.Error + return
func getUser(c *gin.Context) {
u, err := svc.FindUser(c.Param("id"))
if err != nil {
c.Error(errcode.ErrUserNotFound) // 交给中间件
return
}
c.JSON(200, gin.H{"code": 0, "data": u})
}
6.5 panic 恢复与告警
func Recovery(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
logger.Error("panic",
zap.Any("error", err),
zap.ByteString("stack", debug.Stack()),
zap.String("path", c.Request.URL.Path),
)
// 这里可接告警:企业微信/飞书/Sentry
c.AbortWithStatusJSON(500, gin.H{"code": 50000, "message": "服务器内部错误"})
}
}()
c.Next()
}
}
不要用 panic 做流程控制
Go 的惯例是「错误即值」,业务失败返回 error,而非 panic。panic 只留给「真正不该发生」的情况(空指针、越界、配置缺失)。Recovery 是最后一道保险,不是常规分支。
6.6 校验与错误处理小结
- 用翻译器把
ValidationErrors转中文,字段名靠labeltag。 - 定义带 HTTP 映射的业务错误码类型,分层向上传递用
%w包装。 - handler 只
c.Error(),统一错误中间件在链尾集中渲染,代码更干净。 - Recovery 兜底 panic 并打堆栈 + 告警;panic 不做常规控制流。
- 下一章进入 数据库集成——GORM 连接、依赖注入、CRUD 与事务。