参数绑定
概述
本章将深入介绍 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" |
| 邮箱格式 | binding:"email" | |
| url | URL 格式 | binding:"url" |
| ip | IP 地址 | binding:"ip" |
| uuid | UUID 格式 | 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 的参数绑定:
- 绑定来源:查询参数、路径参数、请求头、JSON、表单
- 综合绑定:自动绑定、绑定标签
- 数据验证:基本规则、嵌套结构体、切片验证
- 自定义验证:注册验证器、自定义错误消息
- 文件上传:单文件、多文件
在下一章中,我们将学习 Hertz 的 HTTP 客户端。