在Golang开发过程中,很多业务场景需要对结构体字段进行合法性校验,比如接口入参校验、数据库写入前数据校验等。如果为每个结构体单独编写校验逻辑,会导致大量重复代码,维护成本也会不断升高。利用Golang的反射机制,我们可以构建一个通用的动态校验框架,通过结构体标签定义校验规则,在运行时动态解析规则并完成校验工作。

动态校验框架设计思路
整个框架的核心设计围绕以下几个部分展开:
- 定义统一的校验规则结构体标签,支持多种校验类型,比如非空、长度范围、数值范围等
- 通过反射遍历结构体的所有字段,解析每个字段对应的校验标签
- 根据解析得到的校验规则,匹配对应的校验逻辑,执行字段值的合法性校验
- 收集所有校验失败的字段和错误信息,统一返回给调用方
核心实现步骤
1. 定义校验规则标签
我们首先约定结构体标签的名称为validate,标签内容可以包含多个校验规则,规则之间用分号分隔,每个规则的参数用逗号分隔。比如validate:"required;min=3,max=10"表示字段必填,且长度在3到10之间。
2. 解析结构体标签
通过反射获取结构体的类型信息,遍历每个字段的validate标签,将标签内容解析为规则名称和对应的参数。以下是解析标签的核心代码:
package validator
import (
"reflect"
"strings"
)
// 解析字段的validate标签,返回规则列表,每个规则是map[string]string,包含rule和params
func parseTag(tag string) []map[string]string {
rules := make([]map[string]string, 0)
if tag == "" {
return rules
}
// 按分号分割多个规则
ruleStrs := strings.Split(tag, ";")
for _, ruleStr := range ruleStrs {
ruleStr = strings.TrimSpace(ruleStr)
if ruleStr == "" {
continue
}
rule := make(map[string]string)
// 按等号分割规则名和参数
parts := strings.SplitN(ruleStr, "=", 2)
ruleName := parts[0]
rule["rule"] = ruleName
if len(parts) == 2 {
// 按逗号分割参数
params := strings.Split(parts[1], ",")
rule["params"] = strings.Join(params, ",")
} else {
rule["params"] = ""
}
rules = append(rules, rule)
}
return rules
}
3. 实现校验逻辑
接下来需要根据解析得到的规则,执行对应的校验逻辑。我们定义一个校验函数,接收结构体实例,返回校验错误列表。核心代码如下:
package validator
import (
"fmt"
"reflect"
"strconv"
)
// 校验错误结构体
type ValidateError struct {
Field string // 出错的字段名
Msg string // 错误信息
}
// 校验入口函数,传入结构体指针
func Validate(structPtr interface{}) []ValidateError {
errors := make([]ValidateError, 0)
// 获取反射类型和值
val := reflect.ValueOf(structPtr)
// 必须是指针类型
if val.Kind() != reflect.Ptr {
errors = append(errors, ValidateError{Field: "", Msg: "传入参数必须是指针类型"})
return errors
}
// 获取指针指向的元素
elem := val.Elem()
if elem.Kind() != reflect.Struct {
errors = append(errors, ValidateError{Field: "", Msg: "指针指向的元素必须是结构体"})
return errors
}
// 获取结构体类型
structType := elem.Type()
// 遍历所有字段
for i := 0; i < structType.NumField(); i++ {
field := structType.Field(i)
fieldValue := elem.Field(i)
// 获取validate标签
tag := field.Tag.Get("validate")
if tag == "" {
continue
}
// 解析标签规则
rules := parseTag(tag)
fieldName := field.Name
// 遍历规则执行校验
for _, rule := range rules {
ruleName := rule["rule"]
params := rule["params"]
switch ruleName {
case "required":
// 非空校验
if isEmptyValue(fieldValue) {
errors = append(errors, ValidateError{
Field: fieldName,
Msg: fmt.Sprintf("字段%s不能为空", fieldName),
})
}
case "min":
// 最小长度/最小值校验
if params == "" {
continue
}
minVal, err := strconv.ParseFloat(params, 64)
if err != nil {
continue
}
// 处理字符串长度
if fieldValue.Kind() == reflect.String {
if float64(len(fieldValue.String())) < minVal {
errors = append(errors, ValidateError{
Field: fieldName,
Msg: fmt.Sprintf("字段%s长度不能小于%d", fieldName, int(minVal)),
})
}
} else if isNumberKind(fieldValue.Kind()) {
// 处理数值类型
fieldNum := getNumberValue(fieldValue)
if fieldNum < minVal {
errors = append(errors, ValidateError{
Field: fieldName,
Msg: fmt.Sprintf("字段%s不能小于%v", fieldName, minVal),
})
}
}
}
}
}
return errors
}
// 判断值是否为空
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.String, reflect.Array, reflect.Slice, reflect.Map:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
}
return false
}
// 判断是否为数值类型
func isNumberKind(k reflect.Kind) bool {
switch k {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Float32, reflect.Float64:
return true
}
return false
}
// 获取数值类型的数值
func getNumberValue(v reflect.Value) float64 {
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return float64(v.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return float64(v.Uint())
case reflect.Float32, reflect.Float64:
return v.Float()
}
return 0
}
4. 框架使用示例
定义需要校验的结构体,使用validate标签标注校验规则,然后调用Validate函数即可完成校验:
package main
import (
"fmt"
"validator" // 假设上面的代码放在validator包中
)
type User struct {
Name string `validate:"required;min=2,max=10"`
Age int `validate:"min=1,max=120"`
Email string `validate:"required"`
}
func main() {
user := User{
Name: "a",
Age: 0,
Email: "",
}
errs := validator.Validate(&user)
if len(errs) > 0 {
for _, err := range errs {
fmt.Printf("字段%s校验失败:%sn", err.Field, err.Msg)
}
} else {
fmt.Println("所有字段校验通过")
}
}
框架扩展建议
上述示例只实现了基础的必填和最小值的校验规则,实际使用中可以根据需求扩展更多规则,比如最大值校验、正则匹配、邮箱格式校验等。只需要在switch ruleName的分支中添加对应的规则处理逻辑即可,无需修改框架的整体结构,扩展性较好。同时可以给校验规则支持自定义错误信息,在标签中增加msg参数,解析后替换默认的错误提示,让错误信息更贴合业务场景。