golang中的反射reflect详解

先重复一遍反射三定律:

    1.反射可以将“接口类型变量”转换为“反射类型对象”。

    2.反射可以将“反射类型对象”转换为“接口类型变量”。

    3.如果要修改“反射类型对象”,其值必须是“可写的”(settable)

总结

下面详细说明了Golang的反射reflect的各种功能和用法,都附带有相应的示例,相信能够在工程应用中进行相应实践,总结一下就是:

  • 反射可以大大提高程序的灵活性,使得interface{}有更大的发挥余地

    • 反射必须结合interface才玩得转
    • 变量的type要是concrete type的(也就是interface变量)才有反射一说
  • 反射可以将“接口类型变量”转换为“反射类型对象”

    • 反射使用 TypeOf 和 ValueOf 函数从接口中获取目标对象信息
  • 反射可以将“反射类型对象”转换为“接口类型变量

    • reflect.value.Interface().(已知的类型)
    • 遍历reflect.Type的Field获取其Field
  • 反射可以修改反射类型对象,但是其值必须是“addressable”

    • 想要利用反射修改对象状态,前提是 interface.data 是 settable,即 pointer-interface
  • 通过反射可以“动态”调用方法

  • 因为Golang本身不支持模板,因此在以往需要使用模板的场景下往往就需要使用反射(reflect)来实现

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
package main
 
import (
    "fmt"
    "io"
    "math"
    "os"
    "reflect"
)
 
func main() {
    // 反射的机制就是在运行时动态的调用对象的方法和属性
    // 官方自带的reflect就是反射相关的, golang中的grpc就是通过反射实现的
 
    // 变量包括(type,value)两部分
    // type又分为:static type,concrete type
    //简单来说static type是编码时看见的类型,例如:int string
    // concrete type 是runtime系统看见的类型
    // 类型断言是否能成功取决于concrete type,而不是static type
    // 因此一个reader变量如果他的 concrete type 也实现了write方法的话,它也可以被类型断言为writer
 
    // 反射主要与golang的interface类型相关(它的type是concrete type),只有interface类型才有反射一说
    // 在go中每个interface变量都有一个对应的pair,pair中记录了实际变量的值和类型(value type)
    // value记录了实际变量的值,type记录了实际变量的类型
    // 一个interface类型的变量包含了两个指针,一个指针指向了值得类型(concrete type),一个指针指向了实际的值(value)
 
    // 1. interface和反射
 
    // tty pair<type:*os.File, value:&os.File{}>
    tty, _ := os.OpenFile("./test.txt", os.O_RDWR, 0)
 
    var r io.Reader
    // r pair<type:*os.File, value:&os.File{}>
    // 这个pair在接口变量的连续赋值过程中是不变的
    r = tty
 
    var w io.Writer
    // w pair<type:*os.File, value:&os.File{}>
    // 将这个接口变量r赋值给另外一个接口变量w
    w = r.(io.Writer)
 
    // 接口变量w的pair与r的pair相同,即使w是空接口,pair也是不变的
    fmt.Println(w)
 
    // interface以及pair的存在,是golang中实现反射的前提,理解了pair就更容易理解反射
    // 反射就是用来检测存储在接口变量内部(值value,实际类型concrete type) pair对的一种机制
 
    // 2. golang的反射reflect
    // reflect的基本功能 TypeOf ValueOf
    fmt.Println(reflect.TypeOf(15), reflect.ValueOf(15).Interface().(int))
 
    // reflect.TypeOf是获取pair中的type,reflect.ValueOf是获取pair中的value
    // 也就是说反射可以将 接口类型的变量 转换为 反射类型对象,反射值得是reflect.Type,reflect.Value这两种
 
    // 3. 从reflect.Value中获取接口interface的信息
    // 当执行reflect.ValueOf(interface)之后,就得到了一个类型为reflect.Value变量,可以通过它本身的Interface()方法
    // 获取接口变量的真是内容,然后可以根据类型判断进行转换,转换为原有真实类型,我们可以已知原有类型,也有可能未知原有类型
    // (1) 已知原有类型,进行强制转换
    // realValue := value.Interface().(已知的类型)
    // 示例如下
    var f1 float32 = 3.14567
    pointer := reflect.ValueOf(&f1)
    value := reflect.ValueOf(f1) // 反射将接口类型的变量转换为反射类型的对象  reflect.Value类型
 
    // 可以理解为强制转换,需要注意的是, 转换的时候,如果转换的类型不完全符合,直接panic
    // go对类型要求非常严格,一定要完全符合
    // 如下两个,一个是*float32, 一个是float32,如果弄混直接报错
    // value.Interface()  将反射类型的对象转换为接口类型的变量
    // value.Interface().(float32)  断言接口类型变量的具体类型
    convertPointer := pointer.Interface().(*float32)
    convertValue := value.Interface().(float32)
    fmt.Println(convertPointer, convertValue)
 
    // 说明:转换的时候,如果转换的类型不完全符合,则直接panic,类型要求非常严格
    // 转换的时候要区分指针还是值
    // 也就是说反射可以将反射类型的对象转换为接口类型的变量
 
    // (2) 未知原有类型,【遍历探测其Filed】
    // 案例
    var user = User{1, "lisi", 18}
    DoFieldAndMethod(user)
 
    // 4. reflect.Value 设置实际变量的值
    var f2 = math.Pi
    f2Value := reflect.ValueOf(&f2)  // reflect.Value对象类型
    // reflect.Value.Elem() 获取原始值对应的反射对象,只有原始对象才能修改,当前反射对象是不能修改的
    newValue := f2Value.Elem()  // reflect.Value对象类型
    fmt.Println(newValue.Type(), newValue.CanSet())
    newValue.SetFloat(77)
    fmt.Println(f2)
    /*
    说明
    需要传入的参数是* float64这个指针,然后可以通过pointer.Elem()去获取所指向的Value,注意一定要是指针。
    如果传入的参数不是指针,而是变量,那么
    通过Elem获取原始值对应的对象则直接panic
    通过CanSet方法查询是否可以设置返回false
    newValue.CantSet()表示是否可以重新设置其值,如果输出的是true则可修改,否则不能修改,修改完之后再进行打印发现真的已经修改了。
    reflect.Value.Elem() 表示获取原始值对应的反射对象,只有原始对象才能修改,当前反射对象是不能修改的
    也就是说如果要修改反射类型对象,其值必须是“addressable”【对应的要传入的是指针,同时要通过Elem方法获取原始值对应的反射对象】
    struct 或者 struct 的嵌套都是一样的判断处理方式
     */
 
    // 5. 通过reflect.ValueOf来进行方法的调用
    // (1). 要通过反射来调用起对应的方法,必须要先通过reflect.ValueOf(interface)来获取到reflect.Value,得到“反射类型对象”后才能做下一步处理
    userValue := reflect.ValueOf(user)
    // 一定要指定参数为正确的方法名
    // (2). 先看看带有参数的调用方法
    methodValue := userValue.MethodByName("ReflectCallFunc")
    args := []reflect.Value{reflect.ValueOf(11), reflect.ValueOf("wupeiqi")}
    methodValue.Call(args)
    // (3)在看看不带参数的调用方法
    methodValue = userValue.MethodByName("Call")
    args = make([]reflect.Value, 0)
    methodValue.Call(args)
    /*
    说明
    要通过反射来调用起对应的方法,必须要先通过reflect.ValueOf(interface)来获取到reflect.Value,得到“反射类型对象”后才能做下一步处理
    reflect.Value.MethodByName这.MethodByName,需要指定准确真实的方法名字,如果错误将直接panic,MethodByName返回一个函数值对应的reflect.Value方法的名字。
    []reflect.Value,这个是最终需要调用的方法的参数,可以没有或者一个或者多个,根据实际参数来定。
    reflect.Value的 Call 这个方法,这个方法将最终调用真实的方法,参数务必保持一致,如果reflect.Value'Kind不是一个方法,那么将直接panic。
    本来可以用u.ReflectCallFuncXXX直接调用的,但是如果要通过反射,那么首先要将方法注册,也就是MethodByName,然后通过反射调用methodValue.Call
     */
 
    // 6. Golang的反射reflect性能
    //Golang的反射很慢,这个和它的API设计有关。在 java 里面,我们一般使用反射都是这样来弄的。
    type_ := reflect.TypeOf(user)
    field, _ := type_.FieldByName("Name")
    //这里取出来的 field 对象是 reflect.StructField 类型,但是它没有办法用来取得对应对象上的值。
    //如果要取值,得用另外一套对object,而不是type的反射
    fmt.Println(field.Name)  // 打印的是Name
 
    value_ := reflect.ValueOf(user)
    fieldValue := value_.FieldByName("Name")
    // 这里取出来的 fieldValue 类型是 reflect.Value,它是一个具体的值,而不是一个可复用的反射对象了
    // 每次反射都需要malloc这个reflect.Value结构体,并且还涉及到GC。
    fmt.Println(fieldValue.Interface().(string))
    /* 小结:
    Golang reflect慢主要有两个原因
    涉及到内存分配以及后续的GC;
    reflect实现里面有大量的枚举,也就是for循环,比如类型之类的。
     */
 
}
 
type User struct {
    Id   int
    Name string
    Age  int
}
 
func (u User) ReflectCallFunc(a int, b string) {
    fmt.Println("reflectCallFunc is called!!!")
}
func (u User) Call() {
    fmt.Println("call")
}
 
func DoFieldAndMethod(user interface{}) {
    userType := reflect.TypeOf(user)
    userValue := reflect.ValueOf(user)
    // 注意:方法名和字段名首字母一定要大写,否则获取不到
    // 遍历获取字段名、类型、值
    for i := 0; i < userType.NumField(); i++ {
        field := userType.Field(i)
        value := userValue.Field(i).Interface()
        fmt.Println(field.Name, field.Type, value)
    }
    // 遍历获取方法名和方法类型
    for i := 0; i < userType.NumMethod(); i++ {
        m := userType.Method(i)
        fmt.Println(m.Name, m.Type)
    }
 
    /*
        说明
        通过运行结果可以得知获取未知类型的interface的具体变量及其类型的步骤为:
 
        先获取interface的reflect.Type,然后通过NumField进行遍历
        再通过reflect.Type的Field获取其Field
        最后通过Field的Interface()得到对应的value
        通过运行结果可以得知获取未知类型的interface的所属方法(函数)的步骤为:
 
        先获取interface的reflect.Type,然后通过NumMethod进行遍历
        再分别通过reflect.Type的Method获取对应的真实的方法(函数)
        最后对结果取其Name和Type得知具体的方法名
        也就是说反射可以将“反射类型对象”再重新转换为“接口类型变量”
        struct 或者 struct 的嵌套都是一样的判断处理方式
    */
 
}

  

参考链接:https://studygolang.com/articles/12348?fr=sidebar

 

反射中type和kind的区别

 

posted @   专职  阅读(1177)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示