最近读到一篇关于 Go 反射的文章,作者通过反射给结构体填充字段值的案例,充分利用 Go 的各种内在机理,逐步探讨让代码运行得更快的姿势。
文章(原文地址:https://philpearl.github.io/post/aintnecessarilyslow/)非常有学习价值,故翻译整理了下来。
不要使用反射,除非你真的需要。但是当你不使用反射时,不要认为这是因为反射很慢,它也可以很快。
反射允许你在运行时获得有关 Go 类型的信息。如果你曾经愚蠢地尝试编写 json.Unmarshal 之类的新版本,本文将探讨的就是如何使用反射来填充结构体值。
切入点案例
我们以一个简单的案例为切入点,定义一个结构体 SimpleStruct,它包括两个 int 类型字段 A 和 B。
type SimpleStruct struct { A int B int }
假如我们接收到了 JSON 数据 {"B": 42},想要对其进行解析并且将字段 B 设置为 42。
在下文,我们将编写一些函数来实现这一点,它们都会将 B 设置为 42。
如果我们的代码只适用于 SimpleStruct,这完全是不值一提的。
func populateStruct(in *SimpleStruct) { in.B = 42 }
反射基本版
但是,如果我们是要做一个 JSON 解析器,这意味着我们并不能提前知道结构类型。我们的解析器代码需要接收任何类型的数据。
在 Go 中,这通常意味着需要采用 interface{} (空接口)参数。然后我们可以使用 reflect 包检查通过空接口参数传入的值,检查它是否是指向结构体的指针,找到字段 B 并用我们的值填充它。
代码将如下所示。
func populateStructReflect(in interface{}) error { val := reflect.ValueOf(in) if val.Type().Kind() != reflect.Ptr { return fmt.Errorf("you must pass in a pointer") } elmv := val.Elem() if elmv.Type().Kind() != reflect.Struct { return fmt.Errorf("you must pass in a pointer to a struct") } fval := elmv.FieldByName("B") fval.SetInt(42) return nil }
让我们通过基准测试看看它有多快。
func BenchmarkPopulateReflect(b *testing.B) { b.ReportAllocs() var m SimpleStruct for i := 0; i < b.N; i++ { if err := populateStructReflect(&am