Golang 反射简单应用--参数校验

以下内容为个人学习总结,如果有不准确的地方,欢迎指出!

说实话我之前用Python基本没怎么用过反射,估计在Golang里面也一样,在大多数应用和服务中并不常见。

提到反射,就必须要提一下Golang反射的三大定律

  • 1 可以将interface{}类型转换为reflect类型。
  • 2 通过反射对象可以获取 interface{} 变量。
  • 3 值是否可以被更改,能被寻址。(概念不好理解,后面demo解释)

    原文

    • 1 Reflection goes from interface value to reflection object.
    • 2 Reflection goes from reflection object to interface value.
    • 3 To modify a reflection object, the value must be settable. 三大定律原出处: https://blog.golang.org/laws-of-reflection

反射的一般使用场景

  • 不确定预定类型的参数,需要根据参数的类型来执行不同的操作。

    当然也可以使用Assertion来判断类型,但是这种方式非常麻烦。

// 通过断言判断类型
func AssertionType(v interface{}){
    switch v.(type){
    case int, int16, int32, int64:
        fmt.Printf("整数类型 %d \n", v)
    case userInfo:
        fmt.Printf("是 userInfo 类型 %v  ", v)
        fmt.Printf("年龄 %d \n", v.(userInfo).Age)
        // 断言调用方法
        v.(userInfo).SayAge()
    default:
        fmt.Println("没有匹配到")
    }
}

"但是我们如何处理其它类似[]float64、map[string][]string等类型呢?我们当然可以添加更多的测试分支,但是这些组合类型的数目基本是无穷的。还有如何处理类似url.Values这样的具名类型呢?即使类型分支可以识别出底层的基础类型是map[string][]string,但是它并不匹配url.Values类型,因为它们是两种不同的类型,而且switch类型分支也不可能包含每个类似url.Values的类型,这会导致对这些库的依赖。"--出自Go语言程序设计

没有办法来检查未知类型的表示方式,我们被卡住了。这就是我们为何需要反射的原因。

Golang中反射常用的方法

  • reflect.TypeOf 获取类型 以及相关信息(对应第一条定律)
  • reflect.ValueOf 获取数据的运行时的表示(对应第二条定律)

其他基础方法(部分)

  • 结构体反射常用 假设user为实例化结构体
    • reflect.TypeOf(user).Field(int) // 获取 指定结构体下角标属性的类型
    • reflect.TypeOf(user).Field(int).Tag.Get("foo") // 获取指定下角标属性的结构体标签 内部再调用 Lookup方法
    • reflect.ValueOf(user).NumField() // 获取结构体属性数量
    • reflect.ValueOf(user).Kind() // 获取对应的类型
    • reflect.ValueOf(user).MethodByName("SayName").Call([]reflect.Value{reflect.ValueOf("测试")}) // 传参数调用方法
  • map slice获取获取 (n为map或者slice)
    • reflect.ValueOf(n).Len() // 获取长度

一些关于反射的演示

// 创建一个结构体
type userInfo struct {
    User     string `json:"user"foo:"test_tag"`
    Avatar   string `json:"avatar"bar:""`
    Age      int    `json:"age"`
}
// 定义两个方法
func (u userInfo) SayName(name string){
    fmt.Printf("用户名是 %s\n", name)
}
// 方法
func (u userInfo) SayAge(){
    fmt.Printf("年龄是 %d\n", u.Age)
}

func TestReflect(t *testing.T) {

    // 实例化结构体
    user := userInfo{User: "Nike", Age:18}

    // 断言判断类型 功能不如反射强大
    AssertionType(user)
    AssertionType(1111)

    typ := reflect.TypeOf(user) // 获取reflect的类型
    t.Log(typ)

    val := reflect.ValueOf(user) // 获取reflect的值
    t.Log(val)

    kd := val.Kind() // 获取到st对应的类别
    t.Log(kd)

    num := val.NumField() // 获取值字段的数量
    t.Log(num)

    // 通过反射调用方法
    m1 := val.MethodByName("SayName")
    //m1 := val.Method(0)
    m1.Call([]reflect.Value{reflect.ValueOf("测试")}) // 传参数
    // 私有方法不可反射调用  Java反射可以暴力调用私有方法
    m2 := val.MethodByName("SayAge")
    m2.Call([]reflect.Value{}) // 不传参数

    tagVal := typ.Field(2) // 获取index为2的类型信息
    val = val.Field(2)     // 获取index为2实例化后的值

    t.Log(tagVal)
    t.Log(val)
    t.Log(tagVal.Type) // 输出结构体字段的类型
    t.Log(tagVal.Name) // 输出结构体字段名称

    // 获取结构体的tag 没有则为空 Get 实际就是调用的Lookup
    t.Log(tagVal.Tag.Get("foo"))
    // 返回两个值 第一个为tag值 第二个为bool值 true表示设置了此tag 无论是否为空字符串
    t.Log(tagVal.Tag.Lookup("foo"))
    t.Log(typ.Field(1).Tag.Lookup("bar"))     // 设置了tag为bar 但是为空字符串 依旧为true
    t.Log(typ.Field(1).Tag.Lookup("any_tag")) // 没有设置此tag 就为false

    // 必须使用地址 才可以修改原来的值 否则会panic (反射第三定律,值可以被修改)
    modifyVal(&user)

    t.Log(user)
}

// 通过反射修改值
func modifyVal(user interface{}){

    // 获取变量的指针
    pVal := reflect.ValueOf(user) // 获取reflect的值

    // 获取指针指向的变量
    v := pVal.Elem()
    // 找到并更新变量的值
    v.FieldByName("User").SetString("Jack")

}

反射的简单应用

最简单的例子,通过反射对比slice或者map是否相等

规定Golang中slice,map,func不能用 == 比较 https://stackoverflow.com/questions/37900696/why-cant-go-slice-be-used-as-keys-in-go-maps-pretty-much-the-same-way-arrays-ca https://golang.org/ref/spec#Comparison_operators

sliceA := []int{1, 2, 3}
sliceB := []int{1, 2, 3}
//fmt.Println(sliceA == sliceB) // panic 
// 可以用反射对比
fmt.Println(reflect.DeepEqual(sliceA, sliceB))

利用反射设计一个校验参数的方法

主要参考 gin-vue-admin 参数验证 我自己添加了一个正则校验的方法。 比如定义一个verify来验证结构体里面的方法是否合法

// 存放验证规则的地方 一个字段可以有多个校验方法
type Rules map[string][]string

var (
    UserInfoVerify = Rules{"Page": {Ge("1")}, "PageSize": {Le("50")}, "Name": {Regex(`^\d{3}$`), NotEmpty()}}
)

type UserInfo struct {
    Page     int    `json:"page"`
    PageSize int    `json:"page_size"`
    Name     string `json:"name"`
}

func TestVerify(t *testing.T) {

    // 参数信息  可以和请求的参数信息绑定
    u := UserInfo{Page: 1, PageSize: 30, Name: "234"} // 合法
    //u := UserInfo{Page:1, PageSize:30, Name: "1234"} // Name非法

    // 验证参数是否合法 verify 自定义的校验蚕农书
    if err := verify(u, UserInfoVerify); err != nil {
        t.Log(fmt.Sprintf("验证失败 %s \n", err))
    } else {
        t.Log("success")
    }
}

// 核心函数
func verify(st interface{}, roleMap Rules) (err error) {

    // 限定 比较返回值为 以下几个
    compareMap := map[string]bool{
        "lt": true,
        "le": true,
        "eq": true,
        "ne": true,
        "ge": true,
        "gt": true,
    }

    typ := reflect.TypeOf(st)
    val := reflect.ValueOf(st)

    // 判断待验证参数 是否是结构体 不是直接返回错误
    if val.Kind() != reflect.Struct {
        return errors.New("expect struct")
    }

    // 遍历结构体的所有字段
    for i := 0; i < val.NumField(); i++ {

        // 获取反射后的具体字段
        tagVal := typ.Field(i)
        val := val.Field(i)

        // 判断此字段是否有校验规则 >0 则说明有
        if len(roleMap[tagVal.Name]) > 0 {

            // 循环此字段的校验规则(一个字段可以存在多个校验规则)  规则为 各个判断类型函数 返回值
            for _, v := range roleMap[tagVal.Name] {
                switch {
                // 非空判断
                case v == "notEmpty":
                    if isBlank(val) {
                        return errors.New(tagVal.Name + "值不能为空")
                    }
                // 正则校验
                case strings.Split(v, "=")[0] == "regex":
                    if !isRegexMatch(val.String(), v) {
                        return errors.New(tagVal.Name + "正则校验不合法" + v)
                    }

                // 比较符判断 分割返回值里面的 = 符号  compareMap 确保输入的函数正确
                case compareMap[strings.Split(v, "=")[0]]:
                    // 比较值    val 为反射字段后的值  v为 lt=1等校验值
                    if !compareVerify(val, v) {
                        return errors.New(tagVal.Name + "长度或值不在合法范围," + v)
                    }
                default:
                    fmt.Println("检查 Rules 校验函数输入是否正确: " + v)
                }
            }
        }
    }
    return nil
}

以上代码不完整,完整代码见GitHub地址

反射应用参数校验代码地址

原文地址见个人博客

参数校验GitHub地址


文章作者: 王小右
版权声明: 咳咳想白嫖文章?本文章著作权归作者所有,任何形式的转载都请注明出处。 https://www.charmcode.cn !
还能输入 100 字
  目录