问题
在前后端分离的web服务中,通常需要对前端的请求参数做一些校验,比如年龄、身高范围,密码长度,邮件格式等等,以确保请求参数的合法性。
当前github中go语言相关,stars数最多的第三方库是:github.com/go-playgrou…
go get:
go get github.com/go-playground/validator/v10
import:
import "github.com/go-playground/validator/v10"
这个库支持很多验证规则,详情见:documentation
本文主要是介绍如何给指定tag自定义验证器,以及如何定制错误消息返回给调用方。
相关实现
下面先进行简要说明,然后给出代码实现。
- 将验证规则写到结构体的
tag中,用validate字段标识; - 调用
validator.New().RegisterValidation(tag string, fn Func, ...)函数进行tag和自定义验证器的注册;
// RegisterValidation adds a validation with the given tag
//
// NOTES:
// - if the key already exists, the previous validation function will be replaced.
// - this method is not thread-safe it is intended that these all be registered prior to any validation
func (v *Validate) RegisterValidation(tag string, fn Func, callValidationEvenIfNull ...bool) error {
return v.RegisterValidationCtx(tag, wrapFunc(fn), callValidationEvenIfNull...)
}
- 对验证器验证后得到的
err进行断言err.(validator.ValidationErrors); - 对断言后的
ValidationError进行规则匹配,然后自定义返回给调用方的错误消息;
参考自:validator/examples/simple/main.go
package main
import (
"errors"
"strings"
"testing"
"github.com/go-playground/validator/v10"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
type People struct {
Name string `json:"name" validate:"required"`
// 如果某项为空,不需要验证,需要把omitempty这个tag放在最前面,如下所示。
// 如果写成"isValidAge,omitempty"则不能到达上面的效果
Age int `json:"age" validate:"omitempty,isValidAge"`
Hobby string `json:"hobby" validate:"isValidHobby,oneof=ball swim"`
Height int `json:"height" validate:"max=100,min=10"`
}
const (
isValidAgeTag = "isValidAge"
isValidHobbyTag = "isValidHobby"
)
var (
InvalidAgeErr = errors.New("age should be between 0 and 200")
InvalidHobbyErr = errors.New("hobby should be 'ball' or 'swim'")
)
func validAge(fl validator.FieldLevel) bool {
age := fl.Field().Int()
if age > 0 && age < 200 {
return true
}
return false
}
func validHobby(fl validator.FieldLevel) bool {
hobby := fl.Field().String()
if hobby == "ball" || hobby == "swim" {
return true
}
return false
}
var validateObj *validator.Validate
func registerValidation(tag string, fn validator.Func) {
if err := validateObj.RegisterValidation(tag, fn); err != nil {
logrus.Fatalf("register validator for '%s' error: %v", tag, err)
}
}
func init() {
validateObj = validator.New()
registerValidation(isValidAgeTag, validAge)
registerValidation(isValidHobbyTag, validHobby)
}
func validateStruct(s interface{}) error {
err := validateObj.Struct(s)
if err == nil {
return nil
}
// refer to: https://github.com/go-playground/validator/issues/559
for _, validateErr := range err.(validator.ValidationErrors) {
// only check first error
switch validateErr.Tag() {
case isValidAgeTag:
// return custom error message
return InvalidAgeErr
case isValidHobbyTag:
return InvalidHobbyErr
default:
return err
}
}
return err
}
func TestValidateStruct(t *testing.T) {
cases := []struct {
people People
isValid bool
errContain string
}{
{People{}, false, "Name"},
{People{Name: "tom"}, false, InvalidHobbyErr.Error()}, // 验证 Age => omitempty tag
{People{Name: "tom", Age: -10}, false, InvalidAgeErr.Error()},
{People{Name: "tom", Age: 10}, false, InvalidHobbyErr.Error()},
{People{Name: "tom", Age: 10, Hobby: "ball"}, false, "Height"},
{People{Name: "tom", Age: 10, Hobby: "ball", Height: 10}, true, ""},
}
for _, item := range cases {
err := validateStruct(item.people)
if item.isValid {
assert.NoError(t, err)
} else {
assert.True(t, strings.Contains(err.Error(), item.errContain))
}
}
}
参考
最后
大家好,如果觉得有用,可以悄悄给我点个赞[😄],也可以关注微信公众号「那只猴子」。