reflect 是 Go 标准库里的反射包,可以在运行时查看变量的类型和值,也可以动态读取和修改对象。它常用于 JSON 序列化、ORM、配置解析、参数校验、依赖注入等框架代码中。

什么是反射

正常写 Go 代码时,变量类型在编译期就已经确定:

1
2
name := "Tom"
age := 18

编译器知道 namestringageint,所以可以做类型检查。

反射解决的是另一个问题:如果代码在编译期不知道具体类型,运行时才拿到一个 interface{}any,还能不能知道它到底是什么类型、有什么字段、能不能调用方法?

答案就是使用 reflect

简单来说:

1
2
普通代码:编译期知道类型
反射代码:运行时检查类型和值

reflect 的两个核心类型

reflect 包最重要的是两个类型:

  • reflect.Type:表示变量的类型信息。
  • reflect.Value:表示变量的值信息。

获取方式:

1
2
reflect.TypeOf(x)
reflect.ValueOf(x)

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import (
	"fmt"
	"reflect"
)

func main() {
	var age int = 18

	t := reflect.TypeOf(age)
	v := reflect.ValueOf(age)

	fmt.Println(t)        // int
	fmt.Println(v)        // 18
	fmt.Println(t.Kind()) // int
	fmt.Println(v.Kind()) // int
}

Type 更关注“它是什么类型”,Value 更关注“它的值是什么”。


Type 和 Kind 的区别

Type 表示具体类型,Kind 表示底层分类。

1
2
3
4
5
6
7
8
type MyInt int

var num MyInt = 10

t := reflect.TypeOf(num)

fmt.Println(t)        // main.MyInt
fmt.Println(t.Kind()) // int

MyInt 是一个自定义类型,所以 Typemain.MyInt。但它的底层类型是 int,所以 Kindint

常见 Kind 有:

  • reflect.Int
  • reflect.String
  • reflect.Bool
  • reflect.Struct
  • reflect.Ptr
  • reflect.Slice
  • reflect.Map
  • reflect.Interface
  • reflect.Func

读取基本类型的值

reflect.Value 提供了不同方法读取具体值:

1
2
3
4
5
6
7
8
9
func main() {
	name := "Tom"
	age := 18
	active := true

	fmt.Println(reflect.ValueOf(name).String())
	fmt.Println(reflect.ValueOf(age).Int())
	fmt.Println(reflect.ValueOf(active).Bool())
}

不同类型要用对应方法:

  • 字符串用 String()
  • 整数用 Int()
  • 无符号整数用 Uint()
  • 浮点数用 Float()
  • 布尔值用 Bool()

如果类型不匹配,可能会 panic。所以实际使用时一般先判断 Kind

1
2
3
4
5
v := reflect.ValueOf(age)

if v.Kind() == reflect.Int {
	fmt.Println(v.Int())
}

修改变量的值

反射可以修改变量,但必须传入指针。

错误示例:

1
2
3
4
5
age := 18
v := reflect.ValueOf(age)

// panic: reflect.Value.SetInt using unaddressable value
v.SetInt(20)

原因是 ValueOf(age) 拿到的是变量的副本,不是原变量本身。

正确写法:

1
2
3
4
5
6
7
8
age := 18

v := reflect.ValueOf(&age)
e := v.Elem()

e.SetInt(20)

fmt.Println(age) // 20

这里有两个关键点:

  • ValueOf(&age):传入变量地址。
  • Elem():拿到指针指向的真实值。

修改前也可以判断是否可修改:

1
2
3
if e.CanSet() {
	e.SetInt(20)
}

反射结构体

反射最常见的使用场景之一是处理结构体。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type User struct {
	Name string
	Age  int
}

func main() {
	u := User{Name: "Tom", Age: 18}

	t := reflect.TypeOf(u)
	v := reflect.ValueOf(u)

	for i := 0; i < t.NumField(); i++ {
		fieldType := t.Field(i)
		fieldValue := v.Field(i)

		fmt.Println(fieldType.Name, fieldType.Type, fieldValue)
	}
}

输出类似:

1
2
Name string Tom
Age int 18

常用方法:

  • NumField():字段数量。
  • Field(i):根据下标获取字段。
  • FieldByName(name):根据字段名获取字段。
  • Name:字段名。
  • Type:字段类型。

读取结构体 Tag

结构体 tag 本质上也是类型信息的一部分,所以需要通过 reflect.Type 读取。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type User struct {
	Name string `json:"name" validate:"required"`
	Age  int    `json:"age"`
}

func main() {
	t := reflect.TypeOf(User{})

	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)

		fmt.Println(field.Name)
		fmt.Println("json:", field.Tag.Get("json"))
		fmt.Println("validate:", field.Tag.Get("validate"))
	}
}

这也是很多框架能够识别 jsongormvalidate 等 tag 的基础。

例如 encoding/json 会读取结构体字段上的 json tag,然后决定 JSON 字段名:

1
2
3
type User struct {
	Name string `json:"name"`
}

修改结构体字段

修改结构体字段时,也需要传指针:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
type User struct {
	Name string
	Age  int
}

func main() {
	u := User{Name: "Tom", Age: 18}

	v := reflect.ValueOf(&u).Elem()

	nameField := v.FieldByName("Name")
	if nameField.IsValid() && nameField.CanSet() {
		nameField.SetString("Jack")
	}

	ageField := v.FieldByName("Age")
	if ageField.IsValid() && ageField.CanSet() {
		ageField.SetInt(20)
	}

	fmt.Println(u) // {Jack 20}
}

需要注意,反射只能直接修改导出字段,也就是首字母大写的字段。

1
2
3
4
type User struct {
	Name string
	age  int
}

这里的 age 是未导出字段,普通反射不能直接 Set


调用方法

反射也可以动态调用方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
type User struct {
	Name string
}

func (u User) Say(prefix string) string {
	return prefix + ", " + u.Name
}

func main() {
	u := User{Name: "Tom"}

	v := reflect.ValueOf(u)
	method := v.MethodByName("Say")

	args := []reflect.Value{
		reflect.ValueOf("hello"),
	}

	result := method.Call(args)

	fmt.Println(result[0].String()) // hello, Tom
}

Call 接收 []reflect.Value,返回值也是 []reflect.Value。因为一个函数可能有多个参数,也可能有多个返回值。

调用前最好判断方法是否存在:

1
2
3
4
5
method := v.MethodByName("Say")
if method.IsValid() {
	result := method.Call(args)
	fmt.Println(result[0].String())
}

遍历 map 和 slice

遍历 slice

1
2
3
4
5
6
nums := []int{1, 2, 3}
v := reflect.ValueOf(nums)

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

遍历 map

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
m := map[string]int{
	"Tom":  18,
	"Jack": 20,
}

v := reflect.ValueOf(m)

for _, key := range v.MapKeys() {
	value := v.MapIndex(key)
	fmt.Println(key.String(), value.Int())
}

如果要写通用工具函数,比如打印任意对象内容,反射就很有用。


一个简单的通用打印函数

下面是一个简单示例:根据不同类型打印值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func PrintValue(x any) {
	v := reflect.ValueOf(x)

	switch v.Kind() {
	case reflect.String:
		fmt.Println("string:", v.String())
	case reflect.Int:
		fmt.Println("int:", v.Int())
	case reflect.Bool:
		fmt.Println("bool:", v.Bool())
	case reflect.Struct:
		t := v.Type()
		for i := 0; i < v.NumField(); i++ {
			fmt.Println(t.Field(i).Name, "=", v.Field(i))
		}
	default:
		fmt.Println("unsupported:", v.Kind())
	}
}

使用:

1
2
3
PrintValue("Tom")
PrintValue(18)
PrintValue(User{Name: "Tom", Age: 18})

反射的使用场景

反射适合写通用代码,常见场景包括:

  • JSON、XML 等序列化和反序列化。
  • ORM 框架根据结构体字段生成 SQL。
  • 参数校验库读取 validate tag。
  • 配置库把配置文件映射到结构体。
  • RPC 框架动态调用方法。
  • 依赖注入框架根据类型创建对象。

比如 JSON 序列化时,标准库需要知道结构体有哪些字段、字段名是什么、有没有 json tag,这些都需要依赖反射。


反射的缺点

反射虽然强大,但不应该滥用。

性能更差

反射需要在运行时检查类型、查找字段、做动态调用,相比普通代码性能更低。

可读性更差

普通代码:

1
user.Name = "Tom"

反射代码:

1
v.FieldByName("Name").SetString("Tom")

后者更绕,也更容易写错。

类型安全变弱

普通代码很多错误可以在编译期发现,反射代码很多错误会变成运行时 panic。

例如:

1
v.SetInt(10)

如果 v 不是整数类型,程序运行时才会报错。


使用建议

  • 业务代码里优先使用普通类型和接口。
  • 只有在需要处理未知类型、通用框架、动态字段时再使用反射。
  • 使用前先判断 Kind,避免类型不匹配。
  • 修改值时传指针,并使用 Elem()
  • 修改字段前判断 CanSet()
  • 调用方法前判断 IsValid()
  • 对性能敏感的代码谨慎使用反射。

一句话总结

Go 的 reflect 包提供了运行时检查类型和值的能力,核心是 reflect.Typereflect.Value;它可以读取字段、读取 tag、修改值、调用方法,适合框架和通用工具代码,但性能、可读性和类型安全都不如普通代码,所以业务代码中应该谨慎使用。