Skip to content

参数绑定

概述

本章将深入介绍 Hertz 的参数绑定机制。Hertz 提供了强大的参数绑定功能,支持从请求的多个来源(路径参数、查询参数、请求体、请求头等)绑定数据到结构体,并支持数据验证。

核心内容

绑定来源

查询参数绑定

go
package main

import (
    "context"
    
    "github.com/cloudwego/hertz/pkg/app"
    "github.com/cloudwego/hertz/pkg/app/server"
)

type SearchRequest struct {
    Keyword string `query:"keyword" binding:"required"`
    Page    int    `query:"page" binding:"min=1"`
    Size    int    `query:"size" binding:"min=1,max=100"`
}

func main() {
    h := server.Default()
    
    h.GET("/search", func(c context.Context, ctx *app.RequestContext) {
        var req SearchRequest
        
        if err := ctx.BindQuery(&req); err != nil {
            ctx.JSON(400, map[string]interface{}{
                "code":    400,
                "message": err.Error(),
            })
            return
        }
        
        ctx.JSON(200, map[string]interface{}{
            "keyword": req.Keyword,
            "page":    req.Page,
            "size":    req.Size,
        })
    })
    
    h.Spin()
}

路径参数绑定

go
type UserRequest struct {
    ID   int    `uri:"id" binding:"required,min=1"`
    Name string `uri:"name"`
}

func main() {
    h := server.Default()
    
    h.GET("/users/:id/:name", func(c context.Context, ctx *app.RequestContext) {
        var req UserRequest
        
        if err := ctx.BindUri(&req); err != nil {
            ctx.JSON(400, map[string]interface{}{
                "code":    400,
                "message": err.Error(),
            })
            return
        }
        
        ctx.JSON(200, map[string]interface{}{
            "id":   req.ID,
            "name": req.Name,
        })
    })
    
    h.Spin()
}

请求头绑定

go
type Headers struct {
    ContentType string `header:"Content-Type" binding:"required"`
    Token       string `header:"Authorization" binding:"required"`
    RequestID   string `header:"X-Request-ID"`
}

func main() {
    h := server.Default()
    
    h.POST("/api", func(c context.Context, ctx *app.RequestContext) {
        var headers Headers
        
        if err := ctx.BindHeader(&headers); err != nil {
            ctx.JSON(400, map[string]interface{}{
                "code":    400,
                "message": err.Error(),
            })
            return
        }
        
        ctx.JSON(200, map[string]interface{}{
            "content_type": headers.ContentType,
            "token":        headers.Token,
            "request_id":   headers.RequestID,
        })
    })
    
    h.Spin()
}

JSON 请求体绑定

go
type CreateUserRequest struct {
    Name     string `json:"name" binding:"required,min=2,max=50"`
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required,min=8"`
    Age      int    `json:"age" binding:"omitempty,min=1,max=120"`
}

func main() {
    h := server.Default()
    
    h.POST("/users", func(c context.Context, ctx *app.RequestContext) {
        var req CreateUserRequest
        
        if err := ctx.BindJSON(&req); err != nil {
            ctx.JSON(400, map[string]interface{}{
                "code":    400,
                "message": err.Error(),
            })
            return
        }
        
        ctx.JSON(201, map[string]interface{}{
            "id":    1,
            "name":  req.Name,
            "email": req.Email,
        })
    })
    
    h.Spin()
}

表单绑定

go
type LoginForm struct {
    Username string `form:"username" binding:"required"`
    Password string `form:"password" binding:"required"`
    Remember bool   `form:"remember"`
}

func main() {
    h := server.Default()
    
    h.POST("/login", func(c context.Context, ctx *app.RequestContext) {
        var form LoginForm
        
        if err := ctx.BindForm(&form); err != nil {
            ctx.JSON(400, map[string]interface{}{
                "code":    400,
                "message": err.Error(),
            })
            return
        }
        
        ctx.JSON(200, map[string]interface{}{
            "username": form.Username,
            "remember": form.Remember,
        })
    })
    
    h.Spin()
}

综合绑定

自动绑定

go
type Request struct {
    ID      int    `path:"id" binding:"required"`
    Name    string `query:"name" binding:"required"`
    Token   string `header:"Authorization" binding:"required"`
    Content string `json:"content"`
}

func main() {
    h := server.Default()
    
    h.PUT("/users/:id", func(c context.Context, ctx *app.RequestContext) {
        var req Request
        
        if err := ctx.Bind(&req); err != nil {
            ctx.JSON(400, map[string]interface{}{
                "code":    400,
                "message": err.Error(),
            })
            return
        }
        
        ctx.JSON(200, map[string]interface{}{
            "id":      req.ID,
            "name":    req.Name,
            "token":   req.Token,
            "content": req.Content,
        })
    })
    
    h.Spin()
}

绑定标签说明

go
type Request struct {
    PathParam   string `path:"param"`     // 路径参数
    QueryParam  string `query:"param"`    // 查询参数
    FormParam   string `form:"param"`     // 表单参数
    HeaderParam string `header:"Param"`   // 请求头参数
    JSONField   string `json:"field"`     // JSON 字段
    CookieParam string `cookie:"param"`   // Cookie 参数
}

数据验证

基本验证规则

go
type User struct {
    Name     string `binding:"required"`
    Email    string `binding:"required,email"`
    Password string `binding:"required,min=8,max=32"`
    Age      int    `binding:"omitempty,min=1,max=120"`
    Phone    string `binding:"omitempty,len=11"`
    Score    int    `binding:"gte=0,lte=100"`
    Status   int    `binding:"oneof=0 1 2"`
    URL      string `binding:"omitempty,url"`
    IP       string `binding:"omitempty,ip"`
}

常用验证标签

标签说明示例
required必填binding:"required"
min最小值/长度binding:"min=3"
max最大值/长度binding:"max=100"
len等于长度binding:"len=11"
eq等于binding:"eq=1"
ne不等于binding:"ne=0"
gt大于binding:"gt=0"
gte大于等于binding:"gte=0"
lt小于binding:"lt=100"
lte小于等于binding:"lte=100"
oneof枚举值binding:"oneof=1 2 3"
email邮箱格式binding:"email"
urlURL 格式binding:"url"
ipIP 地址binding:"ip"
uuidUUID 格式binding:"uuid"

嵌套结构体验证

go
type Address struct {
    Province string `json:"province" binding:"required"`
    City     string `json:"city" binding:"required"`
    Street   string `json:"street" binding:"required"`
}

type CreateUserRequest struct {
    Name    string  `json:"name" binding:"required"`
    Email   string  `json:"email" binding:"required,email"`
    Address Address `json:"address" binding:"required"`
}

func main() {
    h := server.Default()
    
    h.POST("/users", func(c context.Context, ctx *app.RequestContext) {
        var req CreateUserRequest
        
        if err := ctx.BindJSON(&req); err != nil {
            ctx.JSON(400, map[string]interface{}{
                "code":    400,
                "message": err.Error(),
            })
            return
        }
        
        ctx.JSON(201, map[string]interface{}{
            "name":    req.Name,
            "email":   req.Email,
            "address": req.Address,
        })
    })
    
    h.Spin()
}

切片验证

go
type CreateOrderRequest struct {
    UserID   int      `json:"user_id" binding:"required,gt=0"`
    Products []int    `json:"products" binding:"required,min=1,dive,gt=0"`
    Tags     []string `json:"tags" binding:"max=5,dive,min=2,max=20"`
}

func main() {
    h := server.Default()
    
    h.POST("/orders", func(c context.Context, ctx *app.RequestContext) {
        var req CreateOrderRequest
        
        if err := ctx.BindJSON(&req); err != nil {
            ctx.JSON(400, map[string]interface{}{
                "code":    400,
                "message": err.Error(),
            })
            return
        }
        
        ctx.JSON(201, map[string]interface{}{
            "user_id":  req.UserID,
            "products": req.Products,
            "tags":     req.Tags,
        })
    })
    
    h.Spin()
}

自定义验证

注册自定义验证器

go
import (
    "github.com/cloudwego/hertz/pkg/app"
    "github.com/go-playground/validator/v10"
)

func init() {
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        v.RegisterValidation("mobile", func(fl validator.FieldLevel) bool {
            mobile := fl.Field().String()
            if len(mobile) != 11 {
                return false
            }
            for _, c := range mobile {
                if c < '0' || c > '9' {
                    return false
                }
            }
            return mobile[0] == '1'
        })
    }
}

type User struct {
    Name   string `json:"name" binding:"required"`
    Mobile string `json:"mobile" binding:"required,mobile"`
}

func main() {
    h := server.Default()
    
    h.POST("/users", func(c context.Context, ctx *app.RequestContext) {
        var user User
        
        if err := ctx.BindJSON(&user); err != nil {
            ctx.JSON(400, map[string]interface{}{
                "code":    400,
                "message": err.Error(),
            })
            return
        }
        
        ctx.JSON(201, user)
    })
    
    h.Spin()
}

自定义错误消息

go
import (
    "github.com/go-playground/validator/v10"
    "github.com/go-playground/locales/zh"
    ut "github.com/go-playground/universal-translator"
    zh_translations "github.com/go-playground/validator/v10/translations/zh"
)

var (
    validate *validator.Validate
    trans    ut.Translator
)

func init() {
    validate = validator.New()
    
    uni := ut.New(zh.New())
    trans, _ = uni.GetTranslator("zh")
    
    zh_translations.RegisterDefaultTranslations(validate, trans)
    
    validate.RegisterTranslation("required", trans, func(ut ut.Translator) error {
        return ut.Add("required", "{0}为必填字段", true)
    }, func(ut ut.Translator, fe validator.FieldError) string {
        t, _ := ut.T("required", fe.Field())
        return t
    })
}

func GetErrorMsg(err error) string {
    if validationErrors, ok := err.(validator.ValidationErrors); ok {
        for _, e := range validationErrors {
            return e.Translate(trans)
        }
    }
    return err.Error()
}

文件上传

单文件上传

go
func main() {
    h := server.Default()
    
    h.POST("/upload", func(c context.Context, ctx *app.RequestContext) {
        file, err := ctx.FormFile("file")
        if err != nil {
            ctx.JSON(400, map[string]interface{}{
                "code":    400,
                "message": "No file uploaded",
            })
            return
        }
        
        filename := fmt.Sprintf("./uploads/%d_%s", time.Now().Unix(), file.Filename)
        
        if err := ctx.SaveUploadedFile(file, filename); err != nil {
            ctx.JSON(500, map[string]interface{}{
                "code":    500,
                "message": "Failed to save file",
            })
            return
        }
        
        ctx.JSON(200, map[string]interface{}{
            "filename": file.Filename,
            "size":     file.Size,
            "path":     filename,
        })
    })
    
    h.Spin()
}

多文件上传

go
func main() {
    h := server.Default()
    
    h.POST("/uploads", func(c context.Context, ctx *app.RequestContext) {
        form, err := ctx.MultipartForm()
        if err != nil {
            ctx.JSON(400, map[string]interface{}{
                "code":    400,
                "message": err.Error(),
            })
            return
        }
        
        files := form.File["files"]
        results := []map[string]interface{}{}
        
        for _, file := range files {
            filename := fmt.Sprintf("./uploads/%d_%s", time.Now().Unix(), file.Filename)
            
            if err := ctx.SaveUploadedFile(file, filename); err != nil {
                continue
            }
            
            results = append(results, map[string]interface{}{
                "filename": file.Filename,
                "size":     file.Size,
            })
        }
        
        ctx.JSON(200, map[string]interface{}{
            "count":  len(results),
            "files":  results,
        })
    })
    
    h.Spin()
}

完整示例

go
package main

import (
    "context"
    "fmt"
    "time"
    
    "github.com/cloudwego/hertz/pkg/app"
    "github.com/cloudwego/hertz/pkg/app/server"
)

type User struct {
    ID       int    `json:"id"`
    Name     string `json:"name" binding:"required,min=2,max=50"`
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required,min=8"`
    Age      int    `json:"age" binding:"omitempty,min=1,max=120"`
}

type SearchQuery struct {
    Keyword string `query:"keyword" binding:"required"`
    Page    int    `query:"page" binding:"omitempty,min=1"`
    Size    int    `query:"size" binding:"omitempty,min=1,max=100"`
}

func main() {
    h := server.Default()
    
    h.GET("/search", func(c context.Context, ctx *app.RequestContext) {
        var query SearchQuery
        query.Page = 1
        query.Size = 10
        
        if err := ctx.BindQuery(&query); err != nil {
            ctx.JSON(400, map[string]interface{}{
                "code":    400,
                "message": err.Error(),
            })
            return
        }
        
        ctx.JSON(200, map[string]interface{}{
            "keyword": query.Keyword,
            "page":    query.Page,
            "size":    query.Size,
        })
    })
    
    h.GET("/users/:id", func(c context.Context, ctx *app.RequestContext) {
        id := ctx.Param("id")
        
        ctx.JSON(200, map[string]interface{}{
            "id":   id,
            "name": "User " + id,
        })
    })
    
    h.POST("/users", func(c context.Context, ctx *app.RequestContext) {
        var user User
        
        if err := ctx.BindJSON(&user); err != nil {
            ctx.JSON(400, map[string]interface{}{
                "code":    400,
                "message": err.Error(),
            })
            return
        }
        
        user.ID = int(time.Now().Unix())
        
        ctx.JSON(201, map[string]interface{}{
            "code": 0,
            "data": user,
        })
    })
    
    h.PUT("/users/:id", func(c context.Context, ctx *app.RequestContext) {
        id := ctx.Param("id")
        
        var user User
        if err := ctx.BindJSON(&user); err != nil {
            ctx.JSON(400, map[string]interface{}{
                "code":    400,
                "message": err.Error(),
            })
            return
        }
        
        ctx.JSON(200, map[string]interface{}{
            "id":      id,
            "name":    user.Name,
            "email":   user.Email,
            "message": "User updated",
        })
    })
    
    h.POST("/upload", func(c context.Context, ctx *app.RequestContext) {
        file, err := ctx.FormFile("file")
        if err != nil {
            ctx.JSON(400, map[string]interface{}{
                "code":    400,
                "message": "No file uploaded",
            })
            return
        }
        
        filename := fmt.Sprintf("./uploads/%d_%s", time.Now().Unix(), file.Filename)
        
        if err := ctx.SaveUploadedFile(file, filename); err != nil {
            ctx.JSON(500, map[string]interface{}{
                "code":    500,
                "message": "Failed to save file",
            })
            return
        }
        
        ctx.JSON(200, map[string]interface{}{
            "filename": file.Filename,
            "size":     file.Size,
        })
    })
    
    h.Spin()
}

小结

本章介绍了 Hertz 的参数绑定:

  1. 绑定来源:查询参数、路径参数、请求头、JSON、表单
  2. 综合绑定:自动绑定、绑定标签
  3. 数据验证:基本规则、嵌套结构体、切片验证
  4. 自定义验证:注册验证器、自定义错误消息
  5. 文件上传:单文件、多文件

在下一章中,我们将学习 Hertz 的 HTTP 客户端。