Go反射reflect包的用法和注意点

反射是 Go 在运行时审视和操作自身类型与值的能力,入口是 reflect.TypeOf 取类型、reflect.ValueOf 取值。读结构体用 NumField 加 Field 遍历,改值必须传指针用 Elem 再 CanSet 否则 panic,还能用 MethodByName 按名字调方法。但反射慢、丢编译检查,是框架利器不是业务日常。

Go 是门静态类型语言,类型在编译期就定死了。但有些场景,我们需要在运行时才知道一个变量到底是什么类型、有哪些字段、能调哪些方法。

这种"程序运行时审视和操作自己"的能力,就是反射(reflection)。

上一篇讲 struct tag 时就用到了反射去读 tag。这篇把 Go 的 reflect 包正经讲一遍:两个核心概念、怎么读、怎么改,以及什么时候别碰它。

反射到底在干嘛

正常写代码,类型是明确的:

var age int = 18
age = age + 1  // 编译器清楚 age 是 int,直接操作
​

但如果有个函数,参数是 interface{}(空接口,啥都能接),函数内部拿到的只是个"不知道具体类型的值",怎么办?

func printAny(v interface{}) {
    // v 可能是 int、string、User 结构体... 运行时才知道
    // 想知道它的类型、取它的字段,就得靠反射
}
​

反射就是解决这个的:在运行时动态地获取类型信息、读写值、调用方法。

JSON 序列化、ORM 把结构体映射成表、gin 绑定参数——这些框架不可能预先知道你的结构体长啥样,全靠反射在运行时现场分析。

两个核心:Type 和 Value

Go 反射的入口就两个函数,对应两个核心概念:

  • reflect.TypeOf(x):拿到 x 的类型信息(它是什么类型)。
  • reflect.ValueOf(x):拿到 x 的值信息(它的值是多少,也能通过它改值)。
import "reflect"

func main() {
    var age = 18

    t := reflect.TypeOf(age)   // 类型
    v := reflect.ValueOf(age)  // 值

    fmt.Println("类型:", t)              // int
    fmt.Println("种类:", t.Kind())       // int(Kind 是底层种类)
    fmt.Println("值:", v)                // 18
    fmt.Println("取回 int:", v.Int())    // 18(按 int 取出来)
}
​

这里有个容易混的点:Type 和 Kind。

  • Type 是具体类型,比如你自定义的 type MyInt int,Type 是 MyInt。
  • Kind 是底层种类,MyInt 的 Kind 还是 int。

判断"这是不是个结构体""是不是个切片",要看 Kind(),不是 Type()。

反射读结构体

最常见的用法是遍历结构体的字段。

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func inspect(u interface{}) {
    t := reflect.TypeOf(u)
    v := reflect.ValueOf(u)

    // 先确认传进来的确实是结构体,不然下面会 panic
    if t.Kind() != reflect.Struct {
        fmt.Println("不是结构体,溜了")
        return
    }

    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)   // 第 i 个字段的类型信息
        value := v.Field(i)   // 第 i 个字段的值
        // field.Tag.Get 读 tag,上一篇讲过
        fmt.Printf("字段=%s 类型=%s 值=%v json标签=%s\n",
            field.Name, field.Type, value, field.Tag.Get("json"))
    }
}

func main() {
    inspect(User{Name: "米虫", Age: 18})
}
​

输出:

字段=Name 类型=string 值=米虫 json标签=name
字段=Age 类型=int 值=18 json标签=age
​

NumField() 拿字段数量,Field(i) 取第 i 个,一遍循环就把整个结构体扒了个底朝天。这就是 ORM 把结构体字段对应到数据库列的基本原理。

反射改值的坑

反射不光能读,还能改值。但这里有个经典大坑,几乎人人栽过。

func main() {
    age := 18
    v := reflect.ValueOf(age)
    v.SetInt(20)  // panic!
}
​

直接这么写会 panic:reflect: reflect.Value.SetInt using unaddressable value。

原因:reflect.ValueOf(age) 传进去的是 age 的拷贝,改拷贝没意义,Go 直接禁止,所以报"不可寻址"。

想改原值,必须传指针进去,再用 Elem() 拿到指针指向的那个"可寻址"的值:

func main() {
    age := 18
    // 传指针 &age
    v := reflect.ValueOf(&age)
    // Elem() 解引用,拿到指针指向的可寻址 Value
    elem := v.Elem()
    // 还要确认它可被设置
    if elem.CanSet() {
        elem.SetInt(20)
    }
    fmt.Println(age)  // 20,原值真的被改了
}
​

记住口诀:要改值,传指针,用 Elem,先 CanSet 再 Set。少一步都可能 panic。

反射调用方法

反射还能在运行时调用方法,连方法名都可以是字符串。

type Calc struct{}

func (c Calc) Add(a, b int) int { return a + b }

func main() {
    c := Calc{}
    v := reflect.ValueOf(c)

    // 按名字拿到方法
    method := v.MethodByName("Add")
    // 参数要包成 []reflect.Value
    args := []reflect.Value{reflect.ValueOf(3), reflect.ValueOf(5)}
    // 调用,返回值也是 []reflect.Value
    result := method.Call(args)

    fmt.Println("Add(3,5) =", result[0].Int())  // 8
}
​

这种"用字符串名字调方法"的能力,是很多插件化、路由分发框架的底层手段。

什么时候别用反射

反射很强,但不是免费的,这几点要掂量:

  • 慢:反射比直接代码慢一个数量级。热点路径、循环里大量用,性能会很难看。能用普通代码就别用反射。
  • 丢编译期检查:类型错误从编译期挪到了运行期,本来编译能发现的 bug,变成线上 panic。
  • 代码难读难维护:满屏 reflect.ValueOf、Field(i),可读性直线下降。

我的原则:写业务逻辑基本用不上反射,它是写框架、写通用库才需要的工具。日常被框架用就行,自己手写之前先问一句"真的非它不可吗"。

实在要用,常见的优化是把反射结果缓存起来(比如结构体字段信息解析一次存着),别每次都重新反射。json 包内部就是这么干的。

小结

反射是 Go 在运行时审视和操作自身类型与值的能力,入口是 reflect.TypeOf(取类型)和 reflect.ValueOf(取值)。

读结构体用 NumField + Field(i) 遍历;改值必须传指针、用 Elem()、先 CanSet 再 Set,否则 panic;还能用 MethodByName + Call 按名字调方法。

但反射慢、丢编译检查、难维护,它是框架和通用库的利器,不是业务代码的日常工具。能不用就不用,非用不可就把结果缓存起来。

搞懂它,JSON、ORM、gin 这些框架"凭空操作你结构体"的魔法,就都解释得通了。

更多推荐

章节目录