数据绑定与验证 #

一、数据绑定概述 #

数据绑定是将请求数据自动映射到结构体的过程。

1.1 绑定来源 #

text
┌─────────────────────────────────────────────────────────┐
│                    数据绑定来源                          │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  路径参数 (param)                                        │
│  └── /users/:id  →  param:"id"                          │
│                                                         │
│  查询参数 (query)                                        │
│  └── /users?page=1  →  query:"page"                     │
│                                                         │
│  表单参数 (form)                                         │
│  └── POST body: name=张三  →  form:"name"               │
│                                                         │
│  JSON参数 (json)                                         │
│  └── POST body: {"name":"张三"}  →  json:"name"         │
│                                                         │
│  XML参数 (xml)                                           │
│  └── POST body: <name>张三</name>  →  xml:"name"        │
│                                                         │
└─────────────────────────────────────────────────────────┘

1.2 绑定优先级 #

text
1. 路径参数 (最高优先级)
2. 查询参数
3. 请求体 (最低优先级)

二、基本绑定 #

2.1 Bind方法 #

go
type User struct {
    Name  string `json:"name" form:"name" query:"name"`
    Email string `json:"email" form:"email" query:"email"`
    Age   int    `json:"age" form:"age" query:"age"`
}

e.POST("/users", func(c echo.Context) error {
    u := new(User)
    if err := c.Bind(u); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }
    
    return c.JSON(http.StatusCreated, u)
})

2.2 JSON绑定 #

go
type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

e.POST("/users", func(c echo.Context) error {
    u := new(User)
    if err := c.Bind(u); err != nil {
        return err
    }
    
    return c.JSON(http.StatusCreated, u)
})

测试:

bash
curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"name":"张三","email":"zhangsan@example.com"}' \
  http://localhost:8080/users

2.3 表单绑定 #

go
type LoginForm struct {
    Username string `form:"username"`
    Password string `form:"password"`
}

e.POST("/login", func(c echo.Context) error {
    form := new(LoginForm)
    if err := c.Bind(form); err != nil {
        return err
    }
    
    return c.JSON(http.StatusOK, form)
})

测试:

bash
curl -X POST \
  -d "username=admin&password=123456" \
  http://localhost:8080/login

2.4 查询参数绑定 #

go
type QueryParams struct {
    Page    int    `query:"page"`
    Size    int    `query:"size"`
    Keyword string `query:"keyword"`
}

e.GET("/users", func(c echo.Context) error {
    q := new(QueryParams)
    if err := c.Bind(q); err != nil {
        return err
    }
    
    return c.JSON(http.StatusOK, q)
})

测试:

bash
curl "http://localhost:8080/users?page=1&size=10&keyword=张"

2.5 路径参数绑定 #

go
type PathParams struct {
    ID string `param:"id"`
}

e.GET("/users/:id", func(c echo.Context) error {
    p := new(PathParams)
    if err := c.Bind(p); err != nil {
        return err
    }
    
    return c.JSON(http.StatusOK, p)
})

三、多来源绑定 #

3.1 混合绑定 #

go
type UserRequest struct {
    ID     string `param:"id"`
    Name   string `json:"name" form:"name"`
    Email  string `json:"email" form:"email"`
    Status string `query:"status"`
}

e.PUT("/users/:id", func(c echo.Context) error {
    req := new(UserRequest)
    if err := c.Bind(req); err != nil {
        return err
    }
    
    return c.JSON(http.StatusOK, req)
})

测试:

bash
curl -X PUT \
  -H "Content-Type: application/json" \
  -d '{"name":"张三","email":"zhangsan@example.com"}' \
  "http://localhost:8080/users/123?status=active"

3.2 绑定顺序 #

go
type Request struct {
    ID string `param:"id" query:"id" form:"id" json:"id"`
}

e.POST("/users/:id", func(c echo.Context) error {
    req := new(Request)
    c.Bind(req)
    return c.JSON(http.StatusOK, req)
})

绑定优先级:param > query > form/json

四、嵌套结构绑定 #

4.1 嵌套结构 #

go
type Address struct {
    City    string `json:"city"`
    Country string `json:"country"`
}

type User struct {
    Name    string  `json:"name"`
    Address Address `json:"address"`
}

e.POST("/users", func(c echo.Context) error {
    u := new(User)
    if err := c.Bind(u); err != nil {
        return err
    }
    
    return c.JSON(http.StatusCreated, u)
})

测试:

bash
curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"name":"张三","address":{"city":"北京","country":"中国"}}' \
  http://localhost:8080/users

4.2 指针嵌套 #

go
type User struct {
    Name    string   `json:"name"`
    Address *Address `json:"address,omitempty"`
}

4.3 切片绑定 #

go
type BatchRequest struct {
    Users []User `json:"users"`
}

e.POST("/users/batch", func(c echo.Context) error {
    req := new(BatchRequest)
    if err := c.Bind(req); err != nil {
        return err
    }
    
    return c.JSON(http.StatusCreated, map[string]int{
        "count": len(req.Users),
    })
})

五、数据验证 #

5.1 配置验证器 #

go
package main

import (
    "net/http"
    "github.com/go-playground/validator/v10"
    "github.com/labstack/echo/v4"
)

type CustomValidator struct {
    validator *validator.Validate
}

func (cv *CustomValidator) Validate(i interface{}) error {
    return cv.validator.Struct(i)
}

func main() {
    e := echo.New()
    e.Validator = &CustomValidator{validator: validator.New()}
    
    e.POST("/users", createUser)
    
    e.Start(":8080")
}

5.2 基本验证 #

go
type User struct {
    Name     string `json:"name" validate:"required"`
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required,min=6,max=20"`
    Age      int    `json:"age" validate:"gte=0,lte=130"`
}

func createUser(c echo.Context) error {
    u := new(User)
    if err := c.Bind(u); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }
    
    if err := c.Validate(u); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }
    
    return c.JSON(http.StatusCreated, u)
}

5.3 验证标签 #

标签 说明 示例
required 必填 validate:"required"
email 邮箱格式 validate:"email"
url URL格式 validate:"url"
min 最小长度/值 validate:"min=6"
max 最大长度/值 validate:"max=20"
gte 大于等于 validate:"gte=0"
lte 小于等于 validate:"lte=100"
gt 大于 validate:"gt=0"
lt 小于 validate:"lt=100"
oneof 枚举值 validate:"oneof=red green blue"
len 长度等于 validate:"len=11"
numeric 数字 validate:"numeric"
alpha 字母 validate:"alpha"
alphanum 字母数字 validate:"alphanum"
datetime 日期时间 validate:"datetime=2006-01-02"

5.4 组合验证 #

go
type User struct {
    Username string `json:"username" validate:"required,min=3,max=20,alphanum"`
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required,min=8,max=20"`
    Phone    string `json:"phone" validate:"omitempty,len=11,numeric"`
    Age      int    `json:"age" validate:"required,gte=0,lte=130"`
    Role     string `json:"role" validate:"required,oneof=admin user guest"`
}

5.5 omitempty #

go
type User struct {
    Name  string `json:"name" validate:"required"`
    Email string `json:"email" validate:"omitempty,email"`
    Phone string `json:"phone" validate:"omitempty,len=11"`
}

六、自定义验证 #

6.1 注册自定义验证 #

go
func main() {
    e := echo.New()
    
    v := validator.New()
    v.RegisterValidation("custom", validateCustom)
    
    e.Validator = &CustomValidator{validator: v}
    
    e.Start(":8080")
}

func validateCustom(fl validator.FieldLevel) bool {
    value := fl.Field().String()
    return strings.HasPrefix(value, "prefix_")
}

6.2 使用自定义验证 #

go
type Request struct {
    Code string `json:"code" validate:"required,custom"`
}

6.3 验证函数示例 #

go
func validatePhone(fl validator.FieldLevel) bool {
    phone := fl.Field().String()
    matched, _ := regexp.MatchString(`^1[3-9]\d{9}$`, phone)
    return matched
}

func validateUsername(fl validator.FieldLevel) bool {
    username := fl.Field().String()
    matched, _ := regexp.MatchString(`^[a-zA-Z][a-zA-Z0-9_]{2,19}$`, username)
    return matched
}

func main() {
    e := echo.New()
    
    v := validator.New()
    v.RegisterValidation("phone", validatePhone)
    v.RegisterValidation("username", validateUsername)
    
    e.Validator = &CustomValidator{validator: v}
    
    e.Start(":8080")
}

七、错误处理 #

7.1 验证错误格式化 #

go
func formatValidationErrors(err error) map[string]string {
    errors := make(map[string]string)
    
    for _, err := range err.(validator.ValidationErrors) {
        field := err.Field()
        tag := err.Tag()
        
        switch tag {
        case "required":
            errors[field] = fmt.Sprintf("%s是必填字段", field)
        case "email":
            errors[field] = fmt.Sprintf("%s必须是有效的邮箱地址", field)
        case "min":
            errors[field] = fmt.Sprintf("%s长度不能小于%s", field, err.Param())
        case "max":
            errors[field] = fmt.Sprintf("%s长度不能大于%s", field, err.Param())
        default:
            errors[field] = fmt.Sprintf("%s验证失败", field)
        }
    }
    
    return errors
}

func createUser(c echo.Context) error {
    u := new(User)
    if err := c.Bind(u); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }
    
    if err := c.Validate(u); err != nil {
        errors := formatValidationErrors(err)
        return c.JSON(http.StatusBadRequest, map[string]interface{}{
            "code":    400,
            "message": "验证失败",
            "errors":  errors,
        })
    }
    
    return c.JSON(http.StatusCreated, u)
}

7.2 中文错误消息 #

go
var errorMessages = map[string]string{
    "required": "是必填字段",
    "email":    "必须是有效的邮箱地址",
    "url":      "必须是有效的URL",
    "min":      "长度不能小于%s",
    "max":      "长度不能大于%s",
    "gte":      "必须大于或等于%s",
    "lte":      "必须小于或等于%s",
    "oneof":    "必须是以下值之一: %s",
}

func getErrorMessage(fe validator.FieldError) string {
    msg, ok := errorMessages[fe.Tag()]
    if !ok {
        return fmt.Sprintf("%s验证失败", fe.Tag())
    }
    
    if fe.Param() != "" {
        return fmt.Sprintf(msg, fe.Param())
    }
    
    return msg
}

八、完整示例 #

go
package main

import (
    "fmt"
    "net/http"
    "regexp"
    "strings"
    "github.com/go-playground/validator/v10"
    "github.com/labstack/echo/v4"
)

type User struct {
    Username string `json:"username" validate:"required,username"`
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required,min=8,max=20"`
    Phone    string `json:"phone" validate:"omitempty,phone"`
    Age      int    `json:"age" validate:"required,gte=0,lte=130"`
    Role     string `json:"role" validate:"required,oneof=admin user guest"`
}

type QueryParams struct {
    Page    int    `query:"page" validate:"gte=1"`
    Size    int    `query:"size" validate:"gte=1,lte=100"`
    Keyword string `query:"keyword" validate:"max=50"`
}

type CustomValidator struct {
    validator *validator.Validate
}

func (cv *CustomValidator) Validate(i interface{}) error {
    return cv.validator.Struct(i)
}

func validatePhone(fl validator.FieldLevel) bool {
    phone := fl.Field().String()
    if phone == "" {
        return true
    }
    matched, _ := regexp.MatchString(`^1[3-9]\d{9}$`, phone)
    return matched
}

func validateUsername(fl validator.FieldLevel) bool {
    username := fl.Field().String()
    matched, _ := regexp.MatchString(`^[a-zA-Z][a-zA-Z0-9_]{2,19}$`, username)
    return matched
}

var errorMessages = map[string]string{
    "required": "是必填字段",
    "email":    "必须是有效的邮箱地址",
    "min":      "长度不能小于%s",
    "max":      "长度不能大于%s",
    "gte":      "必须大于或等于%s",
    "lte":      "必须小于或等于%s",
    "oneof":    "必须是以下值之一: %s",
    "phone":    "必须是有效的手机号码",
    "username": "必须以字母开头,只能包含字母、数字和下划线,长度3-20",
}

func formatErrors(err error) map[string]string {
    errors := make(map[string]string)
    
    for _, fe := range err.(validator.ValidationErrors) {
        field := fe.Field()
        msg, ok := errorMessages[fe.Tag()]
        if !ok {
            msg = fmt.Sprintf("%s验证失败", fe.Tag())
        }
        
        if fe.Param() != "" {
            errors[field] = fmt.Sprintf(msg, fe.Param())
        } else {
            errors[field] = msg
        }
    }
    
    return errors
}

func main() {
    e := echo.New()
    
    v := validator.New()
    v.RegisterValidation("phone", validatePhone)
    v.RegisterValidation("username", validateUsername)
    
    e.Validator = &CustomValidator{validator: v}
    
    e.GET("/users", listUsers)
    e.POST("/users", createUser)
    e.PUT("/users/:id", updateUser)
    
    e.Logger.Fatal(e.Start(":8080"))
}

func listUsers(c echo.Context) error {
    q := new(QueryParams)
    if err := c.Bind(q); err != nil {
        return err
    }
    
    if q.Page == 0 {
        q.Page = 1
    }
    if q.Size == 0 {
        q.Size = 10
    }
    
    if err := c.Validate(q); err != nil {
        return c.JSON(http.StatusBadRequest, map[string]interface{}{
            "code":    400,
            "message": "验证失败",
            "errors":  formatErrors(err),
        })
    }
    
    return c.JSON(http.StatusOK, map[string]interface{}{
        "page":    q.Page,
        "size":    q.Size,
        "keyword": q.Keyword,
        "users":   []string{},
    })
}

func createUser(c echo.Context) error {
    u := new(User)
    if err := c.Bind(u); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }
    
    if err := c.Validate(u); err != nil {
        return c.JSON(http.StatusBadRequest, map[string]interface{}{
            "code":    400,
            "message": "验证失败",
            "errors":  formatErrors(err),
        })
    }
    
    return c.JSON(http.StatusCreated, u)
}

func updateUser(c echo.Context) error {
    id := c.Param("id")
    
    u := new(User)
    if err := c.Bind(u); err != nil {
        return err
    }
    
    if err := c.Validate(u); err != nil {
        return c.JSON(http.StatusBadRequest, map[string]interface{}{
            "code":    400,
            "message": "验证失败",
            "errors":  formatErrors(err),
        })
    }
    
    return c.JSON(http.StatusOK, map[string]interface{}{
        "id":   id,
        "user": u,
    })
}

九、总结 #

数据绑定与验证要点:

要点 说明
Bind 自动绑定请求数据
Validate 验证数据格式
标签 json/form/query/param
验证标签 required/email/min/max等
自定义验证 RegisterValidation
错误处理 格式化验证错误

准备好学习模板渲染了吗?让我们进入下一章!

最后更新:2026-03-28