参数绑定与验证
问题
Go Web 框架中如何进行请求参数绑定和验证?
答案
Gin 绑定方式
// 绑定 JSON Body
type CreateOrderReq struct {
ProductID int `json:"product_id" binding:"required"`
Quantity int `json:"quantity" binding:"required,min=1,max=100"`
Address string `json:"address" binding:"required"`
}
func createOrder(c *gin.Context) {
var req CreateOrderReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
}
// 绑定 Query 参数:/users?page=1&size=10
type ListReq struct {
Page int `form:"page" binding:"gte=1"`
Size int `form:"size" binding:"gte=1,lte=100"`
Name string `form:"name"`
}
func listUsers(c *gin.Context) {
var req ListReq
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
}
// 绑定路由参数
type UserURI struct {
ID int `uri:"id" binding:"required"`
}
func getUser(c *gin.Context) {
var uri UserURI
if err := c.ShouldBindUri(&uri); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
}
// 绑定 Header
type AuthHeader struct {
Token string `header:"Authorization" binding:"required"`
}
常用 Validator Tag
| Tag | 示例 | 说明 |
|---|---|---|
required | binding:"required" | 必填 |
min / max | binding:"min=1,max=100" | 数值范围 / 字符串长度 |
len | binding:"len=11" | 精确长度 |
email | binding:"email" | 邮箱格式 |
oneof | binding:"oneof=male female" | 枚举 |
gt / gte / lt / lte | binding:"gte=0" | 比较 |
dive | binding:"dive,required" | 验证切片每个元素 |
url | binding:"url" | URL 格式 |
自定义验证器
import "github.com/go-playground/validator/v10"
// 自定义验证函数
func validPhone(fl validator.FieldLevel) bool {
phone := fl.Field().String()
matched, _ := regexp.MatchString(`^1[3-9]\d{9}$`, phone)
return matched
}
// 注册到 Gin
func init() {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("phone", validPhone)
}
}
// 使用
type UserReq struct {
Phone string `json:"phone" binding:"required,phone"`
}
自定义错误信息
默认错误信息对用户不友好,需要翻译:
func translateError(err error) map[string]string {
errs := make(map[string]string)
var ve validator.ValidationErrors
if errors.As(err, &ve) {
for _, e := range ve {
field := e.Field()
switch e.Tag() {
case "required":
errs[field] = field + " 不能为空"
case "email":
errs[field] = "邮箱格式不正确"
case "min":
errs[field] = field + " 不能小于 " + e.Param()
default:
errs[field] = field + " 验证失败"
}
}
}
return errs
}
func createUser(c *gin.Context) {
var req CreateUserReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"errors": translateError(err)})
return
}
}
常见面试问题
Q1: ShouldBind 和 Bind 的区别?
答案:
| 方法 | 绑定失败行为 |
|---|---|
ShouldBindJSON | 返回 error,由开发者处理 |
BindJSON | 自动返回 400,调用 c.AbortWithError |
推荐用 ShouldBind 系列,灵活控制错误响应格式。
Q2: 如何验证嵌套结构体?
答案:
type Address struct {
City string `json:"city" binding:"required"`
Street string `json:"street" binding:"required"`
}
type CreateUserReq struct {
Name string `json:"name" binding:"required"`
Address Address `json:"address" binding:"required"` // 自动递归验证
}
validator 会自动递归验证嵌套结构体。切片元素用 dive tag:
type OrderReq struct {
Items []Item `json:"items" binding:"required,min=1,dive"` // 验证每个 Item
}