反射是Go中为数不多的"黑魔法"之一,它让程序在运行时检查和修改自身的类型与值。本文通过实例讲清reflect包的核心用法与性能陷阱。
1. reflect.TypeOf 与 reflect.ValueOf
reflect包的入口只有两个函数:
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.14
t := reflect.TypeOf(x)
v := reflect.ValueOf(x)
fmt.Println("Type:", t) // float64
fmt.Println("Value:", v) // 3.14
fmt.Println("Kind:", t.Kind()) // float64
}
TypeOf返回reflect.Type接口,ValueOf返回reflect.Value结构体。两者都携带完整的类型元信息。
2. Kind —— 底层类型枚举
Kind与Type不同:自定义类型type MyInt int的Type是MyInt,但Kind是int。常用Kind值:
| Kind | 含义 |
|---|---|
| reflect.Int, Int8...Int64 | 整数族 |
| reflect.Float32, Float64 | 浮点族 |
| reflect.String | 字符串 |
| reflect.Struct | 结构体 |
| reflect.Ptr | 指针 |
| reflect.Slice | 切片 |
| reflect.Map | 映射 |
判断逻辑:
v := reflect.ValueOf(something)
switch v.Kind() {
case reflect.Int, reflect.Int64:
fmt.Println("integer:", v.Int())
case reflect.String:
fmt.Println("string:", v.String())
case reflect.Struct:
fmt.Println("struct with", v.NumField(), "fields")
}
3. 通过反射修改值
直接传值进ValueOf是不可寻址的,修改会panic。必须传指针,再用Elem()获取可设置的Value:
func main() {
var x float64 = 3.14
v := reflect.ValueOf(&x) // 传指针
elem := v.Elem() // 获取指向的元素
fmt.Println("CanSet:", elem.CanSet()) // true
elem.SetFloat(2.718)
fmt.Println(x) // 2.718
}
核心规则:只有通过指针的Elem()才能SetXxx。
对整数用SetInt,字符串用SetString,以此类推。类型不匹配会panic。
4. 结构体字段遍历
反射在ORM、序列化框架中最常见的用途就是遍历结构体字段:
type User struct {
Name string `json:"name" db:"user_name"`
Age int `json:"age" db:"user_age"`
role string // 未导出
}
func InspectStruct(v interface{}) {
rv := reflect.ValueOf(v)
rt := reflect.TypeOf(v)
if rt.Kind() == reflect.Ptr {
rv = rv.Elem()
rt = rt.Elem()
}
for i := 0; i < rt.NumField(); i++ {
field := rt.Field(i)
value := rv.Field(i)
fmt.Printf("Field: %-6s Type: %-8s JSON: %s Exported: %v Value: %v\n",
field.Name,
field.Type,
field.Tag.Get("json"),
field.IsExported(),
value,
)
}
}
func main() {
u := User{Name: "Alice", Age: 30, role: "admin"}
InspectStruct(u)
}
输出:
Field: Name Type: string JSON: name Exported: true Value: Alice
Field: Age Type: int JSON: age Exported: true Value: 30
Field: role Type: string JSON: Exported: false Value: admin
注意:未导出字段可以读取但不能通过反射修改。
5. 反射的性能代价
反射操作涉及大量的接口装箱/拆箱和运行时类型检查,性能开销不可忽视。用BenchmarkTest对比:
func BenchmarkDirect(b *testing.B) {
x := 42
for i := 0; i < b.N; i++ {
_ = x + 1
}
}
func BenchmarkReflect(b *testing.B) {
x := 42
v := reflect.ValueOf(&x).Elem()
for i := 0; i < b.N; i++ {
v.SetInt(int64(v.Int() + 1))
}
}
典型结果:反射操作比直接操作慢 50~100倍。
实际建议:
- 初始化阶段用反射做一次类型分析,把结果缓存到
sync.Map - 热路径避免反射,用代码生成(
go generate)替代 encoding/json的性能瓶颈很大程度源于反射,需要高性能JSON时考虑json-iterator或sonic
小结
| 操作 | 函数/方法 |
|---|---|
| 获取类型 | reflect.TypeOf(v) |
| 获取值 | reflect.ValueOf(v) |
| 判断底层类型 | .Kind() |
| 修改值 | ValueOf(&v).Elem().SetXxx() |
| 遍历结构体 | .NumField() + .Field(i) |
| 读取标签 | .Tag.Get("json") |
反射是强大的元编程工具,但也是性能杀手。能在编译期解决的事情,就不要留到运行时。