# 3.8 参数校验与错误上抛 :::tip 概述 校验在契约层用标签声明,错误在业务层用哨兵错误上抛——两头都不写手工 if。本页说明绑定优先级、校验标签用法与错误传播路径。 ::: ## 绑定优先级 请求参数按固定顺序写入,**后写覆盖**: ```text query(GET 参数) → body(JSON 体) → path(路径参数) ``` 最终优先级表现为 **path > body > query**:同名字段以路径参数为准。每个字段同时带 `json`(body)与 `form`(query)标签即可参与两种来源绑定。 绑定引擎的入口在 `router.Bind` 包装函数内,handler 拿到的 req 已完成解析与校验。 ## binding / msg 标签 校验读标准 `binding` 标签,错误消息取 `msg` 标签: ```go // internal/model/dto/example.go type ExampleDetailReq struct { Id int64 `json:"id" binding:"required" msg:"缺少ID" dc:"记录ID"` } type ExampleBatchDeleteReq struct { Ids []int64 `json:"ids" binding:"min=1" msg:"请选择要删除的记录" dc:"ID列表"` } // internal/model/dto/user.go(多规则 + 多消息示例) type UserAddReq struct { Username string `json:"username" binding:"required,min=2,max=30" msg:"请输入用户名;用户名长度需在2~30位"` Password string `json:"password" binding:"required,min=6,max=32" msg:"请输入密码;密码长度需在6~32位"` Mobile string `json:"mobile" binding:"omitempty,phone" msg:"手机号格式不正确"` Email string `json:"email" binding:"omitempty,email" msg:"邮箱格式不正确"` } ``` 常用规则: | 标签 | 含义 | 示例 | |---|---|---| | required | 必填(零值也算缺失) | `binding:"required"` | | min / max | 数值/长度边界 | `binding:"min=1" msg:"请选择记录"` | | oneof | 枚举取值 | `binding:"oneof=1 2"` | | gte / lte | 大于等于 / 小于等于 | `binding:"gte=1,lte=500"` | | omitempty | 空值放行(配合格式规则) | `binding:"omitempty,email"` | | phone / email | 内置格式规则(本项目 validator 扩展) | `binding:"omitempty,phone"` | **多消息映射**(utility/validator):`msg` 以 `;` 分段与 `binding` 多规则**一一对应**(如 `required,min=2,max=30` ↔ `请输入用户名;用户名长度需在2~30位`);只写一段时适用于全部规则;分段缺失按规则默认提示兜底。校验**快速失败**:返回第一条错误,消息含字段提示语。内嵌结构体(PageReq)由引擎递归校验。 校验失败时框架直接以统一信封返回,msg 即上述文案——**不需要 handler 做任何判断**。 ## UnmarshalBody 逃生舱 文件上传等非 JSON 载荷,结构体实现 `UnmarshalBody(c *gin.Context) error` 即可接管解析: ```go // internal/model/dto/example.go func (r *ExampleImportReq) UnmarshalBody(c *gin.Context) error { f, err := c.FormFile("file") if err != nil { return errors.New("请选择文件") } r.File = f return nil } ``` 返回 error 即校验失败,同样走统一错误信封。 ## 业务错误上抛 service / 钩子内业务失败一律返回 apperr 错误或哨兵错误,**原样上抛,不做包装**: ```go // internal/service/auth.go(实际代码) return nil, consts.CodeCaptchaError return nil, consts.CodeUserDisabled // internal/service/category.go(实际代码,格式化消息) return apperr.Newf(consts.CodeParamError.Code(), "上级不能选择自己") ``` response 中间件捕获 error 后: ```text apperr 错误 → 按其 code/msg 直接输出 普通 error → 映射为通用失败码,msg 取错误文本 panic → recovery 捕获,输出 500 信封并打错误栈日志 ``` ## 错误处理禁则 | 禁则 | 原因 | |---|---| | 在 handler 转发 HTTP 状态码 | 统一信封由 response 中间件负责 | | 吞掉错误只记日志 | 上层与前端失去失败感知 | | 包装后重复加前缀 | 钩子与 service 禁止 wrap,错误原样上抛 | | 用 error 做流程控制 | 分支结果用返回值表达,error 只表示失败 | ## 编写检查清单 - [ ] 必填字段有 required + msg - [ ] 零值合法的字段不加 required(等值过滤靠 EqFields 零值跳过) - [ ] 文件字段实现 UnmarshalBody - [ ] 业务失败返回哨兵/apperr,不 fmt.Errorf 包装 - [ ] 不在 handler 手写参数判断 ## 下一步 → [3.9 权限节点配置](/zh/guide/permission.html)