本文仅对go反射结构体的注解和反射调用结构体的绑定函数为实例。
对User的Username字段检查匹配正则^[\u4E00-\u9FA5a-zA-Z0-9_.]{0,40}$
package main
import (
"errors"
"fmt"
"go/types"
"reflect"
"regexp"
"strconv"
"strings"
"time"
)
var flagRange = []string{"range", "in", "regex", "int", "int32", "int64", "int8", "string", "char", "float", "float32", "float64", "decimal", "time.time", "func", "function"}
type User struct {
Username string `validate:"regex,^[\u4E00-\u9FA5a-zA-Z0-9_.]{0,40}$"`
}
func Validate(input interface{}) (bool, string, error) {
vType := reflect.TypeOf(input)
vValue := reflect.ValueOf(input)
valueStr := ""
L:
for i := 0; i < vType.NumField(); i++ {
tagValue := vType.Field(i).Tag.Get("validate")
if whetherLowerCase(tagValue) {
tagValue = strings.ToLower(tagValue)
}
// `validate:""` `validate:"-"` will be ignored
if tagValue == "" || tagValue == "-" {
continue
}
value := vValue.Field(i).Interface()
tmp := strings.Split(tagValue, ",")
flag := strings.ToLower(tmp[0])
if !strings.Contains(strings.Join(flagRange, " "), flag) {
return false,
fmt.Sprintf("while validating field '%s',flag '%s' is not contained in the flagRange '%v'", vType.Field(i).Name, flag, flagRange),
errors.New(fmt.Sprintf("while validating field '%s',flag '%s' is not contained in the flagRange '%v'", vType.Field(i).Name, flag, flagRange))
}
var rule string
if len(tmp) > 1 {
rule = strings.Join(tmp[1:], ",")
}
// regex validate
// regex validate is used to replace superChecker tag
// Name string `superChecker:"key"` <==> Name string `validate:"regex,key"`
if flag == "regex" {
if whetherLowerCase(tagValue) {
rule = strings.ToLower(rule)
}
valueStr = ToString(value)
// rule is raw regex
if strings.HasPrefix(rule, `^`) && strings.HasSuffix(rule, `$`) {
re, er := regexp.Compile(rule)
if er != nil {
return false,
fmt.Sprintf("while validating field '%s' regex '%s' throws an error '%s'", vType.Field(i).Name, strconv.QuoteToASCII(rule), er.Error()),
errors.New(fmt.Sprintf("while validating field '%s' regex '%s' throws an error '%s'", vType.Field(i).Name, strconv.QuoteToASCII(rule), er.Error()))
}
ok := re.MatchString(valueStr)
if !ok {
return false,
fmt.Sprintf("while validating field '%s' regex '%s' but got unmatched value '%s'", vType.Field(i).Name, strconv.QuoteToASCII(rule), valueStr),
nil
}
continue L
}
}
}
return true,"success",nil
}
// whether to convert tag value to lowercase
func whetherLowerCase(tagValue string) bool {
if strings.HasPrefix(tagValue, "regex") && strings.Contains(tagValue, "^") && strings.Contains(tagValue, "$") {
return false
}
return true
}
// ToString Change arg to string
func ToString(arg interface{}, timeFormat ...string) string {
if len(timeFormat) > 1 {
panic(errors.New(fmt.Sprintf("timeFormat's length should be one")))
}
switch v := arg.(type) {
case int:
return strconv.Itoa(v)
case int8:
return strconv.FormatInt(int64(v), 10)
case int16:
return strconv.FormatInt(int64(v), 10)
case int32:
return strconv.FormatInt(int64(v), 10)
case int64:
return strconv.FormatInt(v, 10)
case string:
return v
case float32:
return strconv.FormatFloat(float64(v), 'f', -1, 32)
case float64:
return strconv.FormatFloat(v, 'f', -1, 64)
case time.Time:
if len(timeFormat) == 1 {
return v.Format(timeFormat[0])
}
return v.Format("2006-01-02 15:04:05")
case fmt.Stringer:
return v.String()
case types.Pointer:
return "not for ptr,you might need &ptr"
default:
return ""
}
}
func main() {
u := User{Username:"ft123"}
ok,msg, e :=Validate(u)
if e!=nil{
fmt.Println(e.Error())
return
}
if !ok {
fmt.Println(msg)
return
}
fmt.Println(ok, msg)
}
true, success
package main
import (
"errors"
"fmt"
"reflect"
"regexp"
"strings"
)
type User struct {
Username string `validate:"regex,^[\u4E00-\u9FA5a-zA-Z0-9_.]{0,40}$"`
}
func (u User) Validate() (bool, string, error) {
re, e := regexp.Compile("^[\u4E00-\u9FA5a-zA-Z0-9_.]{0,40}$")
if e != nil {
return false, "fail becaues " + e.Error(), e
}
ok := re.MatchString(u.Username)
if !ok {
return ok, "username fails '^[\u4E00-\u9FA5a-zA-Z0-9_.]{0,40}$' got " + u.Username, nil
}
return ok, "success", nil
}
func ValidateMethods(input interface{}) (bool, string, error) {
vType := reflect.TypeOf(input)
vValue := reflect.ValueOf(input)
var info string
var methodName string
var results []reflect.Value
for i := 0; i < vType.NumMethod(); i++ {
methodName = vType.Method(i).Name
// UserValidate,UserSVValidate
if strings.HasSuffix(methodName, "Validate"){
// all cases will validate methods end with 'Validate' or 'SVValidate'
results = vValue.Method(i).Call(nil)
if len(results) != 3 {
info = fmt.Sprintf("while validating method[%d],named '%s',illegal return values,want 3(bool,string,error) but got %d(%s)", i, methodName, len(results), valueListByType(results))
return false, info, errors.New(info)
}
var er error
ok, msg := results[0].Bool(), results[1].String()
if results[2].IsNil() {
er = nil
} else {
er = results[2].Interface().(error)
}
if ok {
continue
} else {
return ok, msg, er
}
}
}
return true, "success", nil
}
// when input a []reflect.Value{false, 5, 'example'}
// returns 'bool,int,string'
func valueListByType(r []reflect.Value) string {
typs := make([]string, 0)
for _, v := range r {
typs = append(typs, reflect.TypeOf(v.Interface()).String())
}
return strings.Join(typs, ",")
}
func main() {
u := User{Username: "ft123"}
ok, msg, e := ValidateMethods(u)
if e != nil {
fmt.Println(e.Error())
return
}
if !ok {
fmt.Println(msg)
return
}
fmt.Println(ok, msg)
}
上述两例,分别简述了:
1.如何通过注解来对结构体字段验证
2.如果通过结构体绑定的方法验证
下面拓展为:
1.注解支持'regex','func','int','float', 'time.Time'
2.绑定方法支持通过参数,选择匹配需要验证的函数方法
实现工具superChecker
地址为https://github.com/fwhezfwhez/SuperChecker
其中,被绑定函数方法需要包含字符串’Validate’或者’SVValidate’, 匹配字符串格式为SVB[flag1]SVS[flag2]SVS[flag3]
, 其中 执行 checker.ValidateMethods(dest, flag1, flag2, flag3…) 将会验证对应的方法,匹配规则是 HIT.
HIT: [1,2,3,4] hits [1,3,4,56] by [1,3,4]
type O struct {
Username string
}
// v1
func (o O) OLengthValidate() (bool, string, error) {
if o.Username > 5 && o.Username < 100 {
return true, "success", nil
}
return false, "length should be between 5 and 100", nil
}
// v2
func (o O) OValidateSVBCreate() (bool, string, error) {
if o.Username != "" {
return true, "success", nil
}
return false, "length should be between 5 and 100", nil
}
// v3
func (o O) OValidateSVBUpdate() (bool, string, error) {
if o.Username == "admin" {
return false, "admin should not be updated", nil
}
return true, "success", nil
}
// v4
func (o O) OValidateSVBUpdateSVSCreate() (bool, string, error) {
if o.Username == "admin" {
return false, "admin should not be updated", nil
}
return true, "success", nil
}
func main() {
...
// o:=O{Username:"he"}
o := O{Username: "hellworld"}
// v1 will be validated
ok, msg, e := checker.ValidateMethods(o)
// v1,v2,v4 will be validated
ok, msg, e = checker.ValidateMethods(o, "create")
// v1,v3,v4 will be validated
ok, msg, e := checker.ValidateMethods(o, "update")
// v1,v2,v3,v4 will all be validated
ok, msg, e := checker.ValidateMethods(o, "update", "create")
if e != nil {
handle(e)
}
fmt.Println(ok, msg)
}
支持 int
,float
,time.time
,regex
,func
,range
,in
type Order struct {
// TIME
CreatedAt time.Time `validate:"time.time"`
UpdatedAt string `validate:"time.time,2006/01/02 15:04:05"`
// INT
Count int `validate:"int,0:200"`
MaxCount int `validate:"int,:200"`
MinCount int `validate:"int,10:"`
Count2 int `validate:"int64,0:200"`
// FLOAT
RewardRate float64 `validate:"float,0:0.4"`
MaxRewardRate float64 `validate:"float,:0.4"`
MinRewradRate float64 `validate:"float,0:"`
RewardRate2 float64 `validate:"float64,0:0.4"`
RewardRate3 decimal.Decimal `validate:"decimal,0:0.4"`
// REGEX
OrderUsername string `validate:"regex,^[\u4E00-\u9FA5a-zA-Z0-9_.]{0,40}$"`
OrderUsername2 string `validate:"regex,username"`
// RANGE,IN
OrderStatus int `validate:"range,[1,2,3,4]"`
OrderStatusName string `validate:"in,[unpaid,paid,closed]"`
// FUNC, FUNCTION
MailTypeCheckBox string `validate:"func,inAndLength,lengthMoreThan3"`
MailTypeCheckBox2 string `validate:"function,lengthLessThan3|inAndLength"`
}
func main() {
order := Order{
CreatedAt: time.Now(),
UpdatedAt: time.Now().Format("2006/01/02 15:04:05"),
Count: 200,
MaxCount: 90,
MinCount: 10,
Count2: 100,
RewardRate: 0.4,
MaxRewardRate: 0.3,
MinRewradRate: 0.1,
RewardRate2: 0.1,
RewardRate3: decimal.NewFromFloat(0.4),
OrderUsername: "superCheckerValidate",
OrderUsername2: "superCheckerValidate",
OrderStatus: 3,
OrderStatusName: "closed",
MailTypeCheckBox: "midMail",
MailTypeCheckBox2: "midMail",
}
checker := superChecker.GetChecker()
checker.AddFunc(func(in interface{}, fieldName string) (bool, string, error) {
v := superChecker.ToString(in)
maxLength := 7
if len(v) > maxLength {
return false, fmt.Sprintf("while validating field '%s', rule key '%s' over length,want %d ,but got %d", fieldName, "inAndLength", maxLength, len(v)), nil
}
vrange := []string{"midMail", "shenMail", "yundaMail"}
for _, value := range vrange {
if value == v {
return true, "success", nil
}
}
return false, fmt.Sprintf("while validating field '%s', rule key '%s', value '%s' not in '%v'", fieldName, "inAndLength", v, vrange), nil
}, "inAndLength")
checker.AddFunc(func(in interface{}, fieldName string)(bool, string, error){
v := superChecker.ToString(in)
minLength := 3
if len(v) < minLength {
return false, fmt.Sprintf("while validating field '%s', rule key '%s' too short length,want %d ,but got %d", fieldName, "inAndLength", minLength, len(v)), nil
}
return true, "success", nil
},"lengthmorethan3")
checker.AddFunc(func(in interface{}, fieldName string)(bool, string, error){
v := superChecker.ToString(in)
maxLength := 3
if len(v) > maxLength {
return false, fmt.Sprintf("while validating field '%s', rule key '%s' too short length,want %d ,but got %d", fieldName, "inAndLength", maxLength, len(v)), nil
}
return true, "success", nil
},"lengthlessthan3")
ok, msg, er := checker.Validate(order)
if er != nil {
fmt.Println(fmt.Sprintf("got an error, '%s'", er.Error()))
return
}
if !ok {
fmt.Println(fmt.Sprintf("validate fail because of '%s'", msg))
return
}
fmt.Println("success")
}