• go反射的基本介绍


    go反射

    反射可以在程序运行的时候检查代码的元信息,动态的修改代码行为。
    官网地址
    开始之前,得先说说go中的类型。

    类型和接口

    1. 类型
      我们知道,go中的类型是静态类型,变量声明的时候需要指定变量类型(go中也支持类型推断,但这不影响本质)。
      比如
    type UserType int
    var admin UserType
    var visitor int
    
    • 1
    • 2
    • 3

    adminvisitor本质类型是一样的,但他俩的的静态类型不一样,如果不强转的话,两者是不能直接转换的。

    1. 接口
      go中的接口代表了一组方法的集合,接口可以用来存储实际的实现了该接口的值,接口变量不能存储接口变量
      接口变量存储变量的时候,其实存储了一对数据(实际的值,实际的类型信息),例子如下:
    // 定义接口变量
    var r io.Reader
    // 创建实际对象
    tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
    if err != nil {
        return nil, err
    }
    // 接口变量存储实际的值
    r = tty
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    r变量包含了一对数据,(tty,*os.File),通过接口变量可以访问该接口里面的方法,要是将接口变量变为实际类型,需要通过断言。在我们的例子中,代码如下:

    f := r.(*os.File)
    
    • 1

    *os.File实现了除io.Reader中的其他接口的方法,需要通过断言来做,不能直接通过io.Reader接口来做,因为io.Reader只提供它的方法的访问。代码如下:

    var w io.Writer
    w = r.(io.Writer)
    
    • 1
    • 2

    按照我们上面的所说的,上面的断言是可以成功的,因为r里面存储的是一对(实际的值,实际的类型信息),因为*os.File确实实现了io.Writer接口。

    1. interface{}
      它代表一组空方法的集合,可以存储任意值,因为每种类型都有0个或更多的方法。从上面的例子来对比。*os.File实现了io.Reader接口,
      io.Reader接口中只有一个方法,*os.File中有除io.Reader之外的其他方法,io.Reader是可以接收*os.File变量的。所以类比到
      interface{},他就可以接收任意值。
      它不是动态类型,而是静态类型,接口类型的变量总是有相同的静态类型,即使在程序运行的时候,接口类型变量实际存储的值可能会发送变化,但这个值
      总是满足接口的要求,其实就是多态。代码如下:
    func TestAction(t *testing.T) {
        insert := InsertAction{}
        update := UpdateAction{}
        CallAction(insert)
        CallAction(update)
    }
    func CallAction(action Action)  {
        action.DoAction()
    }
    type Action interface {
        DoAction()
    }
    type InsertAction struct {}
    
    func (i InsertAction) DoAction() {
        fmt.Println("InsertAction")
    }
    
    type UpdateAction struct {}
    
    func (i UpdateAction) DoAction() {
        fmt.Println("UpdateAction")
    }
    func (i UpdateAction) DoAction1() {
    fmt.Println("UpdateAction")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    同样的道理,如果把上面例子中的CallAction中的入参变为interface{}的话,也没有问题,但interface{}中没有方法,所以,没有方法可以调。除非利用断言。

    反射

    有了上面基础,反射只是检查存储在接口变量中的那一对数据(实际的值,实际的类型信息)。同样在反射中对应两种类型的变量

    1. Type
    2. Value

    分别获取实际的值,实际的类型信息。对应两种方法来获取,从Value对象是可以拿到Type对象的。

    1. reflect.TypeOf。
    2. reflect.ValueOf。

    要注意,在TypeOf,ValueOf的入参都是interface{}(空接口),反射就通过这个接口变量来检查实际存储的值。

    反射包提供了很多的方法用来操作。官方地址如下:

    https://pkg.go.dev/reflect

    在这里只是说明一下简单的使用,具体的还得去翻官网API,在最后会有一个demo来使用一下反射。下面从几个方面来介绍一下反射。

    1. 从实际类型获取反射相关对象。
    2. 从反射对象获取实际的值。
    3. 通过反射对象来修改实际的值。
    4. 结构体相关的一些方法。

    从实际值获取反射相关对象

    上面已经说了Type和Value,通过TypeOf,ValueOf来获取对应的值,它俩分别对应接口变量中存储的一对数据(实际值,实际类型),其中一个方法比较有意思。kind()方法在Value和Type对象中都有,Kind方法返回Kind类型,Kind类型是int的别名,它的作用是返回变量实际的类型(int,Float64等),怎么理解实际类型呢?

    type MyType int
    
    • 1

    这里的int就是实际类型,MyType就是静态类型。

    在这里插入图片描述

    也就是Kind是不能区分静态类型是实际类型的,但Type可以。

    从反射对象获取实际的值

    可以通过Value对象来访问变量值,Value提供了一些方法可以方法到,Type对象不行,要记住Type获取的是实际值的类型,类型是不能访问到实际的值的。代码如下:

        var i MyKind = 1
    	ofValue := reflect.ValueOf(i)
    	// 对int类型的访问,如果在int类型上调用String,或者Bool这种不属于它类型的就会报错,但interface是可以的。
        // 这里要注意,go是按照最高位数返回,返回类型为int64
    	println(ofValue.Int())
    	println(ofValue.String())
    	println(ofValue.Bool())
        // 返回类型为float64
    	println(ofValue.Float())
       // 返回类型为uint64
    	println(ofValue.Uint())
    	println(ofValue.Interface())
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    因为interface{}可以接受任何值,那么上面的例子中还可以这么写

    func main() {
    	var i MyKind = 1
    	ofValue := reflect.ValueOf(i)
        // 这里返回是实际的值,int()表示实际的类型
    	println(ofValue.Int())
    	// 返回interface利用断言来做,但是得注意,这里断言的是静态类型,如果换为int,就会报错
    	println(ofValue.Interface().(MyKind)) 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    实际的参数不变,只是接受参数的变量类型不变,既然可以利用反射从接口变量中获取实际的值,当然也可以反着来。interface()方法实际就是将实际的值和实际类型打包放在一块,返回了回来,在断言的时候只要保证类型一致就可以直接转换。总之interface()方法是ValueOf的逆方法,它的返回值也是Interface{}

    通过反射对象修改实际值,这个值必须是可以设置的

    代码如下:

    var x float64 = 3.4
    v := reflect.ValueOf(x)
    // 会报错,报错为 panic: reflect.Value.SetFloat using unaddressable value
    v.SetFloat(7.1)
    
    • 1
    • 2
    • 3
    • 4

    Value对象中有CanSet()方法表示是否可以设置值,不能设置值任然设置,就会如上面的例子一样。上面报错的原因是在于v不可设置值。

    ar x float64 = 3.4
    v := reflect.ValueOf(x)
    fmt.Println("settability of v:", v.CanSet()) //结果为true
    
    • 1
    • 2
    • 3

    可设置值比较像可寻址,意思就是,通过变量能找到原始的值,这个属性是由反射对象所持有得原始数据项来决定的。go参数传递都是值传递,所以当做如下的操作时:

    var x float64 = 3.4
    v := reflect.ValueOf(x)
    v.SetFloat(7.1)
    
    • 1
    • 2
    • 3

    其实就是将x 复制了一份给ValueOf,所以,这里反射对象操作的是复制的那份数据,而不是x本身,所以,当SetFloat的时候,就会产生歧义,作为使用者来说,想改的是x,结果x没有变。这就有问题了,可设置性就是为了处理这个问题的。如果想修改要怎么办?

    传递指针。虽然指针也是copy了一份,但是两个指针指向的数据是同一个。这就消除了这种歧义。

    在这里插入图片描述

    有两个注意点:

    1. 创建反射对象的时候,传递的是x的指针。
    2. 指针对象对应的反射对象是不可设置值的。

    第一个上面已经说清楚了,第二个,都已经是指针对象了为啥还不能设置值。

    指针获取的反射对象是不能设置值的,因为想要设置值的不是它,他也是复制的,想要设置的是指针所指向的变量。那么?怎么获取呢?

    // 获取指针对象所指向的变量
    v := p.Elem()
    // true
    fmt.Println("settability of v:", v.CanSet())
    
    • 1
    • 2
    • 3
    • 4

    现在v是一个可以设置值的对象,v就是上面例子中的x,所以现在可以通过v来设置x,因为它代表x。代码如下:

    func main() {
    	var x float64 = 3.4
    	// 传递的是指针
    	p := reflect.ValueOf(&x)
    	//
    	fmt.Println("type of p:", p.Type())
    	fmt.Println("settability of p:", p.CanSet())
    	
    	// 这也是一种获取指针对象所指向的实际值的方法,这里换位.Elem也是可以的。
    	indirect := reflect.Indirect(p)
    	if indirect.CanSet() { 
    		indirect.SetFloat(7.1)
    	}
    	println(x)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    需要注意:利用反射修改值的时候需要传递指针

    结构体相关方法

    反射可以获取结构体里面的字段,tag,方法等等。下面的例子中,获取字段,获取结构体,调用方法。

    type Kinds struct {
    	Name string
    	Age int
    	price float64
    }
    
    func (k *Kinds) Names()  {
    	println(k.Name,"llalla")
    }
    
    func main() {
    	kinds := Kinds{
    		Name:  "12",
    		Age:   1,
    		Price: 2,
    	}
    	// 传递的是指针,先获取指针对象所对应的反射对象,在通过Elem获取真正的对象
    	of := reflect.ValueOf(&kinds).Elem()
    	// 或者真正对象的类型
    	t := of.Type()
    	// 获取字段的数量
    	for i := 0; i < t.NumField(); i++ {
    		// 获取字段,这里是直接在value上获取的
    		field := of.Field(i)
    		fmt.Printf("%d: %s %s = %v\n", i,
    			 // 字段的名称,注意,这里是在Type对象上获取的,要记住Type保存类型信息,Value爆粗实际的值类型
    			t.Field(i).Name, 
    			// 在value上获取
    			field.Type(),
    			// 这里最重要,只有value对象持有真正的数据
    			field.Interface())
    	}
    	
    	// 获取指针对象所对应的反射对象
    	valueOf := reflect.ValueOf(&kinds)
    	t2 := valueOf.Type()
    	// 这里要注意,方法是在 kinds的指针对象上面的,不是在Kind上,所以,如果通过 reflect.ValueOf(&kinds).Elem().Type().NumMethod() 结果为0
    	for i := 0; i < t2.NumMethod(); i++ {
    		method := t2.Method(i)
    		value := method.Func
    		// 调用方法,函数和方法是不一样的,第一个参数为作用域,也就是接受对象,如果是函数的话是不需要的。
    		value.Call([]reflect.Value{valueOf})
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    代码上写了注释,要注意以下几点:

    1. 方法的接受对象要分清(指针还是非指针)

    2. 操作反射对象时要将Type和Value的作用分清,以及自己在操作的时候操作的是什么,比如获取字段的值就不能从Type对象中获取。

    3. 在go中可以看到Python的影子,在go中类型是很灵活的。函数,方法也是有value的。通过反射来调用函数,代码如下:

      func add(a,b int) int  {
      	return a+b
      }
      func main() {
          // 获取add方法对应的反射对象
      	valueOf := reflect.ValueOf(add)
          // 组装参数调用,要注意,函数和方法调用的区别,方法有接收者的。Call的第一个参数是接收者
      	values := []reflect.Value{
      		reflect.ValueOf(10),
      		reflect.ValueOf(12),
      	}
      	res := valueOf.Call(values)
      	for _, item := range res {
      		println(item.Int())
      	}
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16

    博客参考laws-of-reflection
    例子单独写一章


    关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢。

  • 相关阅读:
    java web学习
    写一个flutter程序—记录
    Adobe砸200亿添全家桶新成员,Figma后来居上!
    学生身份标签的识别与风控应用
    网络之眼:XSS与CSRF攻击的深度防范
    vue 使用v-cloak
    远程终端工具Xshell、Xftp传输工具、VMware 、CentOS7的下载、安装和使用教程(完整版)
    Tensorflow笔记———循环神经网络RNN
    数据结构-堆排序Java实现
    3.3 【MySQL】字符集和比较规则的应用
  • 原文地址:https://blog.csdn.net/daliucheng/article/details/126594797