Go: gin validation

这是在做 Golang 项目中的一些实践

传统的校验方式

需要在获取数据后, 写很多if判断语句, 无法复用且非常罗嗦

if a != "" {

}
if len(a) < 10 {

}

gin 的参数校验

gin 使用了 go-playground/validator库, 使用tag声明的方式, 支持http请求request中的各类校验

几种常用参数校验

1. get 参数

gin: Only Bind Query String

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. 路径参数

gin: Bind Uri

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

gin: Bind 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:

  1. https://github.com/gin-gonic/gin/issues/430
  2. 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
}

golang

1032 Words

2021-02-04 08:00 +0800