导航
前言
反射:reflect,指的是程序在运行期间可以拿到一个对象的所有信息,并且可以更新对象中的变量和调用对象的方法。在 Go 语言中,标准库中有一些库用到了反射,比如 encoding/json。
Go 反射介绍
在介绍反射之前,首先我们需要了解在 Go 语言中,任意一个变量都由两部分组成:
- Type: 变量的类型,如 int、int64
- Value: 变量的值,如 1024
判断两个变量是否相等,一方面需要判断变量的类型 Type 是否相同,另一方面需要判断变量的值 Value 是否相同。例如 []int 和 []int64,未初始化时其值都是 nil,但是其类型不相同,所以这两个变量互不相等,这也就导致了在 Go 语言当中有时 nil != nil。
interface{}
在 Go 语言中,反射主要与 interface{} 空接口类型相关。interface{} 约定了零个方法,因此任意类型都实现了 interface{},空接口可以作为一切类型的通用类型来使用,类似于 java 中的 object 对象,可以存储任意类型的值。
因此常常能在 Go 标准库中看到此类函数:
func A(v interface{}) {
...
}
函数 A 可以接受任意类型的参数,实现一种泛型的效果。
简单来说,反射就是用来获取接口变量值中 Value 和 Type 的方法,在 Go 语言中,可以用 reflect 包下面的 reflect.Type 和 reflect.Value 表示变量的类型和值。
小试牛刀
1.使用 reflect 获取到变量的类型和值
// use reflect get variable type and value
func reflectTry() {
num := 100
fmt.Println(reflect.TypeOf(num))
fmt.Println(reflect.ValueOf(num))
s := "hello gopher"
fmt.Println(reflect.TypeOf(s))
fmt.Println(reflect.ValueOf(s))
}
通过 reflect 包提供的 Typeof 和 Valueof 函数,获取并打印变量的 Type 和 Value。
2.通过 Kind 方法转换
func useReflectKind() {
reflectKind(1)
reflectKind(0.01)
reflectKind([]int{1, 2, 3})
reflectKind(map[string]struct{}{})
}
func reflectKind(v interface{}) {
value := reflect.ValueOf(v)
switch value.Kind() {
case reflect.Int:
fmt.Printf("int value is: %d\n", value.Int())
case reflect.Float64:
fmt.Printf("float64 value is: %f\n", value.Float())
case reflect.Slice:
fmt.Printf("slice value is: %v\n", value.Slice(0, 3))
default:
fmt.Printf("defaule type is: %v\n", value)
}
}
在很多标准库及第三方库中都有这样的实现,比如在 gorm 中,实现 Go 数据类型到 MySQL 数据类型转换时就用到了反射,篇幅原因只截取部分,具体自行查看源码。
// Get Data Type for MySQL Dialect
func (s *mysql) DataTypeOf(field *StructField) string {
var dataValue, sqlType, size, additionalType = ParseFieldStructForDialect(field, s)
if sqlType == "" {
switch dataValue.Kind() {
case reflect.Bool:
sqlType = "boolean"
case reflect.Int8:
if s.fieldCanAutoIncrement(field) {
field.TagSettingsSet("AUTO_INCREMENT", "AUTO_INCREMENT")
sqlType = "tinyint AUTO_INCREMENT"
} else {
sqlType = "tinyint"
}
...
}
}
}
3. 获取结构体字段和方法
package entity
type User struct {
Id int64 `json:"id"`
Name string `json:"name"`
Age int64 `json:"age"`
country string `json:"country"`
}
// User 值接收者方法
func (u User) GetId() int64 {
return u.Id
}
// *User 指针接收者方法
func (u *User) GetName() string {
return u.Name
}
func (u *User) GetAge() int64 {
return u.Age
}
func (u *User) getCountry() string {
return u.country
}
func reflectStruct(v interface{}) {
value := reflect.ValueOf(v)
typ := value.Type()
fmt.Printf("v type is %v\n", typ)
// 判断该变量是否是结构体类型
if typ.Kind() == reflect.Struct {
// 获取成员变量个数
fieldCount := typ.NumField()
for i := 0; i < fieldCount; i++ {
field := typ.Field(i)
// CanInterface 未导出的成员变量返回 false,获取不到具体的值,否则会导致 panic
if value.Field(i).CanInterface() {
fmt.Printf("field name is: %v, field type is %v, field value is: %v\n", field.Name, field.Type, value.Field(i).Interface())
} else {
fmt.Printf("field name is: %v, field type is %v\n", field.Name, field.Type)
}
}
}
// 注意,对于值接收者和指针接受者来说,方法有不一样的可见性
// 传入的是结构体的拷贝的话,只能获取到值接收者的方法,传入指针可以获取到所有包外可见的方法
fmt.Println(typ.NumMethod())
for i := 0; i < typ.NumMethod(); i++ {
method := typ.Method(i)
fmt.Printf("v metnod name is: %s, type is %s\n", method.Name, method.Type)
}
}
首先定义了一个 User 的结构体,并且该结构体在不同的 package 下面。当传入函数的对象 Type 为 Struct 时,进入处理流程。同样也是 Go 官方提供的函数、方法,去获取到结构体的属性。注意,对于值接收者和指针接受者来说,方法有不一样的可见性,传入的是结构体对象的拷贝的话,只能获取到值接收者的方法,传入指针可以获取到所有包外可见的方法。
4.反射动态修改值
func reflectUpdateField() {
// 修改变量的值
str := "hello gopher"
tStr := reflect.TypeOf(&str)
fmt.Println(tStr.Elem()) // tStr.Elem() 只有 tStr.Kind() 为 Array、Chan、Map、Ptr、Slice时才能调用,否则 panic
vStr := reflect.ValueOf(&str)
if vStr.Elem().CanSet() { // vStr.Elem() 只有 vStr.Kind() 为 Ptr、Interface 时才可以调用,否则 panic
vStr.Elem().SetString("hello world")
}
fmt.Printf("now str is: %s\n", str)
// 修改结构体成员变量的值
user := entity.User{
Id: 1,
Name: "gopher",
Age: 10,
}
vName := reflect.ValueOf(&user.Name)
vName.Elem().SetString("tom")
vAge := reflect.ValueOf(&user.Age)
vAge.Elem().SetInt(100)
fmt.Printf("now user is: %+v\n", user)
}
通过反射修改对象的值,这是反射功能强大的一个体现,但前提是这个值时可设置的,比如对于结构体未导出的变量,包外即无法修改。同时一些限制已经在代码注释中标识出来了,大家灵活使用。
5.获取结构体字段标识
func reflectGetTag() {
user := entity.User{
Id: 1,
Name: "gopher",
Age: 10,
}
table := make(map[string]string)
tUser := reflect.TypeOf(&user)
fmt.Println(tUser.Kind())
tUser = reflect.TypeOf(&user).Elem()
fmt.Println(tUser.Kind())
for i := 0; i < tUser.NumField(); i++ {
table[tUser.Field(i).Name] = tUser.Field(i).Tag.Get("json") // User 结构体中的 json tag,也可以自定义
}
fmt.Printf("tag table is: %+v", table)
}
在 Go 语言中,支持在结构体字段上定义一些字段标识 tag,这样在结构体序列化的时候可以进行一些更灵活的定制。例如 json 标准库读取 json tag,xorm 框架读取 xorm 的 tag,同样可以通过反射去获取到字段的 tag 信息。
6. 动态调用方法
func useReflectMethodCall() {
methodName := "GetId"
user := entity.User{
Name: "gopher",
}
reflectMethodCall(&user, methodName)
}
func reflectMethodCall(v interface{}, methodName string) {
tUser := reflect.TypeOf(v)
_, ok := tUser.MethodByName(methodName)
if ok {
vUser := reflect.ValueOf(v)
method := vUser.MethodByName(methodName)
resp := method.Call([]reflect.Value{reflect.ValueOf(int64(10))})
res := resp[0]
fmt.Println(res.Int())
}
}
该示例通过给定方法名,实现了对给定的 user 对象的动态调用方法。
总结
通过调用 go 官方提供的 reflect 包,可以实现基础的反射操作,但就目前而言,go 的反射与 java 相比还是有比较大的差距。同时,使用反射的操作相对来说效率会偏低,只有在一些框架如 orm 框架等需要兼容多个数据源的场景下才能发挥出反射的优势,平时业务开发用的比较少。但还是很有必要掌握 Go 反射的语言,说不定就用上了呢。最后,欢迎前往我的 github 仓库查看源码,go 反射使用, 有问题可以评论区讨论。