Go 反射的使用


前言

反射: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 反射使用, 有问题可以评论区讨论。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值