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地址

posted @ 2021-01-22 21:30  王小右  阅读(822)  评论(0编辑  收藏  举报