Go 反射浅析-看完就懂

1,372 阅读4分钟

什么是反射

反射是指程序在运行期间,动态地更新、获取变量的值,包括获取字段类型、名称、调用类变量对应的方法等。

使用反射,不需要在编译时就确定变量的类型,而可以在运行时去动态地获取,这更灵活。

reflect 包的使用

Value 与 Type

对于对象,是由 类型 和 值 两部分组成的。

相应地 reflect 包也分为两大部分 valuetype ,并提供了两种初始化函数 TypeOf()ValueOf(),用于获取对应的 类型 和 值。

而对于值,它一定是依附于类型而存在的,故值一定有对应的类型。因此可通过 ValueOf().Type 获取对应的类型,其效果等同于 TypeOf()

读取

通过 Value.Kind() 来获取变量的类型,当然 Type.Kind() 也可以。

Array、Slice

使用 reflect.Value.Len() 获取数组长度,然后使用 reflect.Value.Index() 获取数组中的元素

v := reflect.ValueOf(i)
for i:=0; i<v.Len(); i++ {
  fmt.Println(v.Index(i))
}

还可使用 reflect.Value.Cap() 获取其容量大小

Struct

field

使用 reflect.Value.NumField() 获取结构体中成员个数。用 reflect.Value.Field() 获取结构体中的成员。用 reflect.Value.Field().Tag 获取结构体中的成员的 tag

v := reflect.ValueOf(i)
for i:=0; i<v.NumField(); i++ {
  fmt.Println(v.Index(i))
}
method

对于结构体中的方法,可用 NumMethodMethod 方法来操作。

而方法的参数、返回值,则用 NumOutOut 来操作

当然也可使用 Call 来调用该方法

v := reflect.ValueOf(i)
for i:=0; i<v.NumMethod(); i++ {
  fmt.Println(v.Index(i))
  fmt.Printf("has %d out and %d in", v.Index(i).NumOut(), v.Index(i).NumIn())
}

虽然 reflect.TypeOf().Methodreflect.ValueOf().Method 均能表示对应的方法,但两者略有不同。

Type.Method 表示的是一个描述了返回值、参数、方法名称等信息的结构体,不绑定到任何对象上;而 Value.Method 在前者的基础上,还绑定了相应的额对象(receiver),可以调用 Call 方法。

修改

在修改对象前,我们得先判断是否能修改,涉及以下两个方法:

CanAddr

是否可取地址。

Go 中部分类型的变量是不可寻址的:

  • Map:map 内部会变动其元素
  • String:string 是不可变值,修改了就会重新创建整个 string
  • 常量
  • 中间值
  • 等等
CanSet

判断某对象是否可修改。

只有当对象可取地址 且 可导出(为 struct 的 filed 时) 时,才能被修改

确认对象可修改后,可使用reflect.ValudOf(&x).Elem().SetInt 或者 reflect.ValudOf(&x).Elem().Set(reflect.ValueOf(66)) 来修改对象的值

当然,除了 SetInt 还有 SetStringSetBytes 等方法,也可用 SetCap 来修改数组对象的容量

Go 反射原理

为什么 reflect 能获取到变量的类型,就得先了解 Go 中变量的数据格式。

image-20210920152124220.png

在 Go 中,变量的内部结构分为两大部分:类型 和 值。相应地,反射对象的内部结构分为 类型 与 值。

Go 反射通过将 变量 强制类型转化 为反射对象,然后读取反射对象中的 ValueType 的值,就拿到变量相关的信息。

应用:json 编码

Go 中的 json 包,能将任意数据类型编码为 json 字符串。

我们可通过实现 json 编码器,来练习 reflect 包的使用,具体实现如下:

func Marshal(v interface{}) (string, error) {
	return marshal(reflect.ValueOf(v))
}

func marshal(v reflect.Value) (string, error) {
	switch v.Kind() {
	case reflect.String:
		return "\"" + v.String() + "\"", nil
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return fmt.Sprintf("%d", v.Int()), nil

	case reflect.Float32, reflect.Float64:
		return fmt.Sprintf("%f", v.Float()), nil

	case reflect.Array, reflect.Slice:
		var retArray []string
		for i := 0; i < v.Len(); i++ {
			retString, err := marshal(v.Index(i))
			if err != nil {
				return "", err
			}
			retArray = append(retArray, retString)
		}
		return "[" + strings.Join(retArray, ",") + "]", nil

	case reflect.Bool:
		if v.Bool() {
			return "true", nil
		} else {
			return "false", nil
		}

	case reflect.Map:
		var retArray []string
		for v.MapRange().Next() {
			key, err1 := marshal(v.MapRange().Key())
			if err1 != nil {
				return "", err1
			}
			value, err2 := marshal(v.MapRange().Value())
			if err2 != nil {
				return "", err1
			}
			retArray = append(retArray, "\""+key+"\":"+value)
		}
		return fmt.Sprintf("{%s}", strings.Join(retArray, ",")), nil

	case reflect.Struct:
		var retArray []string
		for i := 0; i < v.NumField(); i++ {
			value, err := marshal(v.Field(i))
			if err != nil {
				return "", err
			}
			retArray = append(retArray, fmt.Sprintf("\"%s\":%s", v.Type().Field(i).Name, value))
		}
		return fmt.Sprintf("{%s}", strings.Join(retArray, ",")), nil

	default:
		return "", errors.New("unsupport type")
	}
}

调用 Marshal 方法,将变量编码为 json 字符串。

type Person struct {
	age   int64
	Name  string 
	Hobby []string
}

func main() {
	p := Person{
		age:  20,
		Name: "www",
		Hobby: []string{
			"a", "b",
		},
	}

	s, err := Marshal(p)
	fmt.Println(s, err)
}

输出为

{"age":20,"Name":"www","Hobby":["a","b"]}

反射的缺点

虽然反射使用方便,功能强大,但是滥用它也会造成一些问题

使得程序可读性更差

由于反射是在程序运行时去获取变量类型并解析之,所以在开发阶段 IDE 无法知道其类型,无法给出有效的代码提示;在编译阶段无法获知其类型,编译时的类型错误检查机制也就失效了。

大量使用反射导致程序性能低

编写两个函数,用反射、直接读取 两种方式读取变量的值

import (
	"reflect"
	"testing"
)

func BenchmarkNormal(t *testing.B) {
	for i := 0; i < t.N; i++ {
		Normal(1)
	}
}

func BenchmarkRef(t *testing.B) {
	for i := 0; i < t.N; i++ {
		Ref(1)
	}
}

func Normal(n int64) {
	_ = n
}

func Ref(n int64) {
	_ = reflect.ValueOf(n).Int()
}

压测结果如下

BenchmarkNormal-16      1000000000               0.2375 ns/op
BenchmarkRef-16         242498898                4.589 ns/op

结果来看,使用反射,运行速度慢约 1 个数量级。

那么究竟慢在哪里?使用 profiler 来看看

image-20210920111336463.png

从图中看到,使用反射获取变量的值,要经过很多步骤,比如拷贝值、拼装相应的反射对象、类型转换等。经过这些操作就不如直接读取变量值来得快。

参考资料

longlog.me/2019/10/23/…

图解go反射实现原理

legendtkl.com/2016/08/06/…