标准库 reflect 为 Go 语言提供了运行时动态获取对象的类型和值以及动态创建对象的能力。反射可以帮助抽象和简化代码,提高开发效率。
但是使用反射势必会多出大量的操作指令,导致性能下降
案例
字段赋值方式对比
type Student struct {
Name string
Id int32
Addr string
Number string
}
func BenchmarkSetStudentValue(b *testing.B) {
student := new(Student)
b.ResetTimer()
for i := 0; i < b.N; i++ {
student.Name = "张三"
student.Id = 10
student.Addr = "缅甸"
student.Number = "10086"
}
}
func BenchmarkSetStudentValueByField(b *testing.B) {
typ := reflect.TypeOf(Student{})
v := reflect.New(typ).Elem()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v.Field(0).SetString("张三")
v.Field(1).SetInt(10)
v.Field(2).SetString("缅甸")
v.Field(3).SetString("10086")
}
}
func BenchmarkSetStudentValueByFieldName(b *testing.B) {
typ := reflect.TypeOf(Student{})
v := reflect.New(typ).Elem()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v.FieldByName("Name").SetString("张三")
v.FieldByName("Id").SetInt(10)
v.FieldByName("Addr").SetString("缅甸")
v.FieldByName("Number").SetString("10086")
}
}
可以看出反射所带来的性能消耗是非常大的,如果使用FieldByName用字段名赋值还会多出一部分通过字段名映射字段索引的反射操作流程,导致性能比使用Field还要劣化17倍左右。
考虑优化
这里可以优化一下,自己维护一个字典
func BenchmarkSetStudentValueByFieldNameCache(b *testing.B) {
typ := reflect.TypeOf(Student{})
v := reflect.New(typ).Elem()
cache := make(map[string]int, typ.NumField())
for i := 0; i < typ.NumField(); i++ {
cache[typ.Field(i).Name] = i
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
v.Field(cache["Name"]).SetString("张三")
v.Field(cache["Id"]).SetInt(10)
v.Field(cache["Addr"]).SetString("缅甸")
v.Field(cache["Number"]).SetString("10086")
}
}
可以看到,快了不少,比直接使用FieldByName,优化了5倍多。
让人眼前一亮的unsafe
顺便提一下先前说的unsafe包,在这个场景下也有用武之地
func BenchmarkSetStudentValueByUnsafe(b *testing.B) {
student := new(Student)
pStudent := unsafe.Pointer(student)
b.ResetTimer()
for i := 0; i < b.N; i++ {
pNameStudent := (*string)(unsafe.Pointer(uintptr(pStudent) + unsafe.Offsetof(student.Name)))
*pNameStudent = "张三"
pIdStudent := (*int)(unsafe.Pointer(uintptr(pStudent) + unsafe.Offsetof(student.Id)))
*pIdStudent = 10
pAddrStudent := (*string)(unsafe.Pointer(uintptr(pStudent) + unsafe.Offsetof(student.Addr)))
*pAddrStudent = "缅甸"
pNumberStudent := (*string)(unsafe.Pointer(uintptr(pStudent) + unsafe.Offsetof(student.Number)))
*pNumberStudent = "10086"
}
}
可以看到,它的性能表现和直接赋值不相上下,让人眼前一亮
总结
反射在一些场景下非常方便,使代码看起来更加简洁精炼,易维护,但同时它带来的性能损耗也是昂贵的。因此能不用反射尽量不用反射,如果一定要用反射,考虑是否可以做一些优化。