Go: gin validation
这是在做 Golang 项目中的一些实践
传统的校验方式
需要在获取数据后, 写很多if
判断语句, 无法复用且非常罗嗦
if a != "" {
}
if len(a) < 10 {
}
gin 的参数校验
gin 使用了 go-playground/validator库, 使用tag声明的方式, 支持http请求request中的各类校验
几种常用参数校验
1. get 参数
curl -X GET “localhost:8085/testing?name=eason&address=xyz”
type Person struct {
Name string `form:"name"`
Address string `form:"address"`
}
var person Person
if c.ShouldBindQuery(&person) == nil {
log.Println("====== Only Bind By Query String ======")
log.Println(person.Name)
log.Println(person.Address)
}
2. 路径参数
curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
package main
import "github.com/gin-gonic/gin"
type Person struct {
ID string `uri:"id" binding:"required,uuid"`
Name string `uri:"name" binding:"required"`
}
func main() {
route := gin.Default()
route.GET("/:name/:id", func(c *gin.Context) {
var person Person
if err := c.ShouldBindUri(&person); err != nil {
c.JSON(400, gin.H{"msg": err})
return
}
c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID})
})
route.Run(":8088")
}
3. json body
type Login struct {
User string `json:"user" binding:"required"`
Password string `json:"password" binding:"required"`
}
var json Login
if err := c.ShouldBindJSON(&json); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
4. header
curl -H “rate:300” -H “domain:music” 127.0.0.1:8080/
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
type testHeader struct {
Rate int `header:"Rate"`
Domain string `header:"Domain"`
}
func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
h := testHeader{}
if err := c.ShouldBindHeader(&h); err != nil {
c.JSON(200, err)
}
fmt.Printf("%#v\n", h)
c.JSON(200, gin.H{"Rate": h.Rate, "Domain": h.Domain})
})
r.Run()
}
json body是一个对象数组
ShouldBindJSON
并不会校验对象数组中的每个对象是否符合要求, 需要自行调用方法处理
可以抽象出一个通用的函数
var ErrNotArray = errors.New("validate array fail, only support array")
func ToSlice(array interface{}) ([]interface{}, error) {
v := reflect.ValueOf(array)
if v.Kind() != reflect.Slice {
return nil, ErrNotArray
}
l := v.Len()
ret := make([]interface{}, l)
for i := 0; i < l; i++ {
ret[i] = v.Index(i).Interface()
}
return ret, nil
}
func ValidateArray(data interface{}) (bool, string) {
array, err := ToSlice(data)
if err != nil {
return false, err.Error()
}
if len(array) == 0 {
return false, "the array should contain at least 1 item"
}
for index, item := range array {
if err := binding.Validator.ValidateStruct(item); err != nil {
message := fmt.Sprintf("data in array[%d], %s", index, ValidationErrorMessage(err))
return false, message
}
}
return true, "valid"
}
使用时
var body []action
if err := c.ShouldBindJSON(&body); err != nil {
// bad request
return
}
if valid, message := common.ValidateArray(body); !valid {
// bad request: message
return
}
更友好的错误提示
当validation报错的时候, 我们期望得到一个更友好的提示信息, 便于使用者确认问题
reference:
- https://github.com/gin-gonic/gin/issues/430
- https://medium.com/@seb.nyberg/better-validation-errors-in-go-gin-88f983564a3d
以下是一种实现, 处理的常用的 validation 规则的错误展示
package util
import (
"fmt"
"io"
"github.com/go-playground/validator/v10"
log "github.com/sirupsen/logrus"
)
// 这里是通用的 FieldError 处理, 如果需要针对某些字段或struct做定制, 需要自行定义一个
type ValidationFieldError struct {
Err validator.FieldError
}
func (v ValidationFieldError) String() string {
e := v.Err
switch e.Tag() {
case "required":
return fmt.Sprintf("%s is required", e.Field())
case "max":
return fmt.Sprintf("%s cannot be longer than %s", e.Field(), e.Param())
case "min":
return fmt.Sprintf("%s must be longer than %s", e.Field(), e.Param())
case "email":
return "Invalid email format"
case "len":
return fmt.Sprintf("%s must be %s characters long", e.Field(), e.Param())
case "gt":
return fmt.Sprintf("%s must greater than %s", e.Field(), e.Param())
case "gte":
return fmt.Sprintf("%s must greater or equals to %s", e.Field(), e.Param())
case "lt":
return fmt.Sprintf("%s must less than %s", e.Field(), e.Param())
case "lte":
return fmt.Sprintf("%s must less or equals to %s", e.Field(), e.Param())
case "oneof":
return fmt.Sprintf("%s must be one of '%s'", e.Field(), e.Param())
}
return fmt.Sprintf("%s is not valid, condition: %s", e.Field(), e.ActualTag())
}
func ValidationErrorMessage(err error) string {
if err == io.EOF {
return "EOF, json decode fail"
}
validationErrs, ok := err.(validator.ValidationErrors)
if !ok {
message := fmt.Sprintf("json decode or validate fail, err=%s", err)
log.Info(message)
return message
}
// currently, only return the first error
for _, fieldErr := range validationErrs {
return ValidationFieldError{fieldErr}.String()
}
return "validationErrs with no error message"
}
判断: 空值还是没有传递
有些场景, 如果一个json对象中, 某个字段可以为空, 但是非空的时候, 要执行某些特殊的逻辑
此时, 使用默认的validation是无法判断, 请求中没有这个字段, 还是传了空值;
// validate
var body action
err := c.ShouldBindBodyWith(&body, binding.JSON)
if err != nil {
// bad request, ValidationErrorMessage(err)
return
}
var data map[string]interface{}
err = c.ShouldBindBodyWith(&data, binding.JSON)
if err != nil {
// bad request, ValidationErrorMessage(err)
return
}
if _, ok := data["name"]; !ok {
// do something
}