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的区别
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)