一、反射基础
反射是指在运行时动态地获取变量的类型(type)和值(value),并且可以修改变量的值或调用其方法。在golang中,通过reflect包实现反射功能。
package main
import (
"fmt"
"reflect"
)
func main() {
var num float64 = 1.234
// 获取变量的类型和值
value := reflect.ValueOf(num)
kind := reflect.TypeOf(num).Kind()
fmt.Println("type:", kind, "| value:", value)
// 尝试修改变量的值
// This will panic at runtime!
value.SetFloat(3.1415926)
fmt.Println("num after modify:", num)
}
运行上面示例代码,输出结果为:
type: float64 | value: 1.234
panic: reflect.Value.SetFloat using unaddressable value
可以看到,虽然可以获取变量num的类型和值,但是修改变量的值会引发panic异常,因为变量num的地址是不可寻址的(unaddressable)。
二、反射中的Kind和Type
反射中的Kind和Type是两个重要的概念。
Kind表示变量的基础类型,它是一个枚举类型(reflect.Kind),包括了所有基础类型及数组、结构体等复杂类型。
Type表示变量的静态类型,它描述的是变量的完整类型信息,包括类型名、包名、方法集等,并且与Kind不同,Type是可以寻址的。
可以通过reflect.Value.Interface()方法获取变量的静态类型。下面的例子程序演示了Kind和Type的使用方法。
package main
import (
"fmt"
"reflect"
)
type MyStruct struct {
Name string
}
func main() {
var num float64 = 1.234
str := "string"
arr := [...]int{1, 2, 3, 4, 5}
s := MyStruct{"struct"}
values := []interface{}{num, str, arr, &s}
for _, value := range values {
kind := reflect.ValueOf(value).Kind()
tp := reflect.TypeOf(value)
fmt.Printf("value :%v\t| kind: %v\t| type:%v\n", value, kind, tp)
}
}
运行上面的代码,输出:
value :1.234 | kind: float64 | type:float64
value :string | kind: string | type:string
value :[5]int | kind: array | type:[5]int
value :&{struct} | kind:ptr | type:*main.MyStruct
可以看到,不同类型变量的Kind和Type是不同的。
三、反射修改值
使用反射可以方便地获取已知类型变量的值和类型信息,同时也可以修改其值或调用方法。
在golang中,使用反射修改变量值时,必须满足以下几个条件:
- 变量必须是可寻址的(addressable)
- 变量必须是可修改的(settable)
下面的示例代码演示了如何使用反射修改变量值。由于在golang中,只有通过指针才能将变量的地址暴露出来并满足可修改性,因此示例代码中所有变量都用指针类型定义。
package main
import (
"fmt"
"reflect"
)
type MyStruct struct {
Name string
}
func main() {
num1 := 2
s := MyStruct{"struct"}
num2 := &num1
p := &s
values := []interface{}{num2, p}
for _, value := range values {
val := reflect.ValueOf(value).Elem()
if val.CanSet() {
switch val.Kind() {
case reflect.Int:
val.Set(reflect.ValueOf(10))
case reflect.Ptr:
elem := val.Elem()
switch elem.Kind() {
case reflect.Struct:
field := elem.FieldByName("Name")
if field.Kind() == reflect.String {
field.SetString("new name")
}
}
}
}
fmt.Println(value)
}
}
运行上面的代码,输出:
10
&{new name}
可以看到,通过反射可以方便地修改变量的值。记住,要可修改,必须要可寻址。
四、反射获取方法和调用方法
使用反射可以很方便地获取变量的方法集并调用其方法。
在golang中,使用反射获取方法集时,需要使用reflect.Type来描述类型信息,并且仅通过类型信息可能不足以描述完整的方法集,因此反射还提供了reflect.ValueOf和reflect.MethodByName等函数来获取方法和调用。
下面示例代码演示了如何使用反射获取方法和调用方法。
package main
import (
"fmt"
"reflect"
)
type MyStruct struct {
Name string
}
func (s *MyStruct) SetName(name string) {
s.Name = name
}
func (s *MyStruct) GetName() string {
return s.Name
}
func main() {
s := MyStruct{"struct"}
fmt.Println(s)
val := reflect.ValueOf(&s)
setValue := val.MethodByName("SetName")
getName := val.MethodByName("GetName")
args := []reflect.Value{reflect.ValueOf("new name")}
setValue.Call(args)
name := getName.Call([]reflect.Value{})
fmt.Println(name[0].String())
fmt.Println(s)
}
运行上面的代码,输出:
{struct}
new name
{new name}
可以看到,使用反射获取方法集并调用方法是非常方便的。
五、反射和接口的关系
golang中的接口类型(interface{})是非常重要的数据类型,所有自定义的接口类型都需要实现该类型,实现原理就是动态类型和动态值之间的关系。
使用反射可以很方便地操作接口类型的数据。下面的示例代码演示了如何使用反射获取接口类型的动态类型和值。
package main
import (
"fmt"
"reflect"
)
type MyInterface interface {
SayHello(name string) string
}
type MyStruct struct {
Name string
}
func (s *MyStruct) SayHello(name string) string {
return fmt.Sprintf("Hello %s, I'm %s.", name, s.Name)
}
func main() {
s := MyStruct{"struct"}
var myInt MyInterface = &s
// 获取接口的动态类型和值
val := reflect.ValueOf(myInt).Elem()
tp := reflect.TypeOf(myInt).Elem()
method := val.MethodByName("SayHello")
args := []reflect.Value{reflect.ValueOf("name")}
result := method.Call(args)
fmt.Println(tp.Name())
fmt.Println(result[0].String())
}
运行上面的代码,输出:
MyInterface
Hello name, I'm struct.
可以看到,使用反射可以很方便地获取接口类型的信息,方法集和调用方法。