Go 反射入门:概念、应用场景与 JSON 序列化原理

我们聊一个问题:

复制代码
为什么 json.Marshal 可以接收任意结构体?

比如:

复制代码
json.Marshal(User{})
json.Marshal(Product{})
json.Marshal([]Order{})

json.Marshal 在编译时并不知道你会传什么类型。

但是它运行时却能知道:

  • 你传进来的是不是结构体
  • 结构体有几个字段
  • 字段叫什么
  • 字段值是多少
  • 字段上有没有 json tag
  • 哪些字段要忽略
  • 哪些字段空值不输出

这背后靠的就是 Go 的反射。

一句话先说结论:

复制代码
反射就是程序在运行时查看和操作类型信息、字段信息、方法信息和值。

它让程序在"不提前知道具体类型"的情况下,仍然可以写出通用逻辑。

一、什么是反射

平时写 Go 代码,大多数类型信息在编译期就确定了。

例如:

复制代码
type User struct {
	Name string
	Age  int
}

func PrintUser(u User) {
	fmt.Println(u.Name)
	fmt.Println(u.Age)
}

这里编译器很清楚:

复制代码
u 是 User
u 有 Name 字段
u 有 Age 字段
Name 是 string
Age 是 int

所以你可以直接写:

复制代码
u.Name
u.Age

但是如果函数参数是:

复制代码
func Print(v any) {}

问题就来了:

复制代码
v 可能是 User
v 可能是 Product
v 可能是 []int
v 可能是 map[string]string
v 也可能是 nil

编译期不知道具体类型,普通代码就没法直接写:

复制代码
v.Name

因为 any 不保证有 Name 字段。

这时就可以用反射,在运行时问它:

复制代码
你到底是什么类型?
你是什么种类?
你有哪些字段?
字段值是多少?
字段上有没有 tag?
能不能修改?

二、Go 反射的两个入口:TypeOf 和 ValueOf

Go 的反射主要在 reflect 包里。

最常用的两个函数是:

复制代码
reflect.TypeOf(v)
reflect.ValueOf(v)

可以这样理解:

复制代码
TypeOf  看类型信息
ValueOf 看值信息

完整例子:

复制代码
package main

import (
	"fmt"
	"reflect"
)

func main() {
	var x any = 123

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

	fmt.Println("type:", t)
	fmt.Println("kind:", t.Kind())
	fmt.Println("value:", v)
	fmt.Println("value kind:", v.Kind())
}

输出:

复制代码
type: int
kind: int
value: 123
value kind: int

这里的 x 是一个接口值,里面实际装的是 int

reflect.TypeOf(x) 拿到的是动态类型:

复制代码
int

reflect.ValueOf(x) 拿到的是运行时值:

复制代码
123

三、Type 和 Kind 有什么区别

反射里经常看到两个词:

复制代码
Type
Kind

它们很像,但不是一回事。

Type 是具体类型。

Kind 是底层分类。

看例子:

复制代码
package main

import (
	"fmt"
	"reflect"
)

type UserID int

func main() {
	var id UserID = 100

	t := reflect.TypeOf(id)

	fmt.Println("type:", t)
	fmt.Println("name:", t.Name())
	fmt.Println("kind:", t.Kind())
}

输出:

复制代码
type: main.UserID
name: UserID
kind: int

解释一下:

复制代码
Type 是 main.UserID
Kind 是 int

因为 UserID 是你定义的新类型,但它的底层种类是 int

再看结构体:

复制代码
type User struct {
	Name string
}

它的:

复制代码
Type 是 main.User
Kind 是 struct

新手可以这样记:

复制代码
Type 更具体:你到底叫什么类型
Kind 更粗略:你属于哪一类

常见 Kind 包括:

复制代码
reflect.Bool
reflect.Int
reflect.String
reflect.Struct
reflect.Slice
reflect.Map
reflect.Ptr
reflect.Interface

四、遍历结构体字段

反射最常见的用途之一,就是遍历结构体字段。

普通代码里,如果你知道类型,可以直接访问字段:

复制代码
user.Name
user.Age

但如果你写的是通用函数,不知道传进来是什么结构体,就要用反射。

示例:

复制代码
package main

import (
	"fmt"
	"reflect"
)

type User struct {
	Name string
	Age  int
}

func PrintFields(v any) {
	rv := reflect.ValueOf(v)
	rt := reflect.TypeOf(v)

	if rv.Kind() != reflect.Struct {
		fmt.Println("not a struct")
		return
	}

	for i := 0; i < rv.NumField(); i++ {
		fieldInfo := rt.Field(i)
		fieldValue := rv.Field(i)

		fmt.Printf("%s = %v\n", fieldInfo.Name, fieldValue)
	}
}

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

输出:

复制代码
Name = Tom
Age = 18

这里几个 API 很重要:

复制代码
rv.NumField()

返回结构体有几个字段。

复制代码
rt.Field(i)

返回第 i 个字段的类型信息,比如字段名、字段类型、tag。

复制代码
rv.Field(i)

返回第 i 个字段的值。

简单理解:

复制代码
reflect.Type 负责字段说明书
reflect.Value 负责字段实际值

五、struct tag 是什么

结构体 tag 是写在字段后面的元信息。

例如:

复制代码
type User struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

这里:

复制代码
`json:"name"`
`json:"age"`

就是 tag。

tag 不会直接改变字段本身。

它更像写给框架或标准库看的说明:

复制代码
这个字段转 JSON 时叫 name
这个字段转 JSON 时叫 age

读取 tag 也要用反射。

示例:

复制代码
package main

import (
	"fmt"
	"reflect"
)

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:", field.Name)
		fmt.Println("json tag:", field.Tag.Get("json"))
		fmt.Println("validate tag:", field.Tag.Get("validate"))
		fmt.Println("---")
	}
}

输出:

复制代码
field: Name
json tag: name
validate tag: required
---
field: Age
json tag: age
validate tag:
---

常见 tag 有:

复制代码
json:"name"
yaml:"name"
db:"name"
gorm:"column:name"
validate:"required"
form:"name"

不同库会读取不同 tag。

例如:

  • encoding/json 读取 json
  • ORM 可能读取 dbgorm
  • 参数校验库可能读取 validate
  • Web 框架可能读取 form

一句话:

复制代码
tag 是结构体字段上的说明书,反射是读取说明书的工具。

六、类型断言和反射有什么区别

你可能会问:

复制代码
v.(T)

不是也能知道类型吗?

是,但它和反射不是一回事。

类型断言适合这种场景:

复制代码
我知道它可能是 string,帮我确认一下。

例如:

复制代码
package main

import "fmt"

func Print(v any) {
	if s, ok := v.(string); ok {
		fmt.Println("string:", s)
		return
	}

	if n, ok := v.(int); ok {
		fmt.Println("int:", n)
		return
	}

	fmt.Println("unknown")
}

func main() {
	Print("hello")
	Print(100)
	Print(true)
}

输出:

复制代码
string: hello
int: 100
unknown

这里你提前写死了:

复制代码
string
int

反射适合这种场景:

复制代码
我不知道它是什么结构体,但我想遍历它的字段和 tag。

对比一下:

能力 类型断言 反射
是否需要提前知道目标类型 需要 不需要
能否遍历未知结构体字段 不适合 可以
能否读取 struct tag 不行 可以
性能 通常更好 通常更慢
可读性 更简单 更复杂
常见用途 判断少数已知类型 JSON、ORM、校验器、框架

所以优先级一般是:

复制代码
能用普通代码就用普通代码
能用接口就用接口
能用类型断言就用类型断言
确实需要通用运行时类型处理,再用反射

七、反射的三条直觉规则

Go 官方博客《The Laws of Reflection》里讲过反射的几个基本规律。

新手可以先不用背原文,记住这三个直觉:

1. 从普通值到反射对象

复制代码
v := reflect.ValueOf(x)
t := reflect.TypeOf(x)

你把普通值交给反射,得到反射对象。

2. 从反射对象回到普通值

复制代码
x := v.Interface()

Interface() 可以把 reflect.Value 重新变成 any

然后你可以做类型断言:

复制代码
s := x.(string)

3. 想修改值,必须传指针并且值可设置

看例子:

复制代码
package main

import (
	"fmt"
	"reflect"
)

func main() {
	name := "Tom"

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

	if v.CanSet() {
		v.SetString("Jerry")
	}

	fmt.Println(name)
}

输出:

复制代码
Jerry

为什么要传 &name

因为如果你只传:

复制代码
reflect.ValueOf(name)

反射拿到的是一份值,不知道原变量在哪里,不能修改原变量。

传指针后:

复制代码
reflect.ValueOf(&name).Elem()

反射才能找到原变量的位置。

一句话:

复制代码
反射修改值时,要拿到可寻址、可设置的值。

八、反射的应用场景

反射不是日常业务代码里到处用的东西。

它更常出现在框架和通用库里。

1. JSON / XML / YAML 序列化

例如:

复制代码
json.Marshal(user)

encoding/json 要在运行时读取结构体字段、字段值和 json tag。

2. ORM 数据库映射

例如:

复制代码
type User struct {
	ID   int    `db:"id"`
	Name string `db:"name"`
}

ORM 可以通过反射知道:

复制代码
User.ID 对应数据库 id 字段
User.Name 对应数据库 name 字段

3. 参数校验

例如:

复制代码
type RegisterRequest struct {
	Email string `validate:"required,email"`
	Age   int    `validate:"gte=18"`
}

校验库会读取 validate tag,检查字段是否合法。

4. 配置解析

例如把配置文件填充到结构体:

复制代码
type Config struct {
	Port int    `yaml:"port"`
	Mode string `yaml:"mode"`
}

配置库通过反射知道字段名、字段类型,再把文本值转换进去。

5. RPC / Web 框架参数绑定

Web 框架经常支持:

复制代码
func CreateUser(req CreateUserRequest) {}

框架要把 HTTP 请求里的 JSON、form、query 参数绑定到结构体字段,也需要读取类型和 tag。

6. 通用调试工具

比如打印任意结构体字段、比较两个值、深拷贝、对象转 map 等。

这些工具往往不知道具体类型,所以会使用反射。

九、反射的缺点

反射很强,但不要滥用。

主要缺点有:

1. 代码复杂

普通代码:

复制代码
user.Name

反射代码:

复制代码
v.FieldByName("Name")

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

2. 运行时才发现问题

普通代码字段写错,编译器会报错。

反射代码字段名写错,可能运行时才发现。

3. 性能更差

反射需要运行时检查类型和值,通常比直接访问字段慢。

对于普通业务逻辑,不要为了"高级"而使用反射。

4. 容易 panic

例如:

复制代码
v.Field(100)
v.SetString("x")

如果字段不存在,或者值不可设置,就可能 panic。

所以反射代码经常要检查:

复制代码
Kind()
IsValid()
CanSet()
CanInterface()
IsNil()

十、为什么 JSON 序列化需要反射

现在回到核心问题:

复制代码
json.Marshal(v any)

它的参数是 any

这表示你可以传任意类型:

复制代码
json.Marshal(User{})
json.Marshal(Product{})
json.Marshal([]int{1, 2, 3})
json.Marshal(map[string]string{"name": "Tom"})

encoding/json 在编译期不知道你会传什么。

所以它必须在运行时做类似这些事:

复制代码
1. 判断传进来的是 struct、slice、map、string、int 还是 bool
2. 如果是 struct,就遍历字段
3. 读取字段值
4. 读取 json tag
5. 忽略 json:"-" 的字段
6. 处理 json:"name,omitempty"
7. 递归处理嵌套结构
8. 生成 JSON 字符串

例如:

复制代码
type User struct {
	Name     string `json:"name"`
	Age      int    `json:"age"`
	Password string `json:"-"`
}

json.Marshal 要知道:

复制代码
Name 字段输出成 name
Age 字段输出成 age
Password 字段忽略

这些都来自结构体字段和 tag。

普通代码不知道未知结构体有哪些字段,只能靠反射。

十一、标准库 json.Marshal 示例

先看标准库自己的行为:

复制代码
package main

import (
	"encoding/json"
	"fmt"
)

type User struct {
	Name     string `json:"name"`
	Age      int    `json:"age"`
	Email    string `json:"email,omitempty"`
	Password string `json:"-"`
}

func main() {
	user := User{
		Name:     "Tom",
		Age:      18,
		Email:    "",
		Password: "secret",
	}

	data, err := json.Marshal(user)
	if err != nil {
		fmt.Println("marshal error:", err)
		return
	}

	fmt.Println(string(data))
}

输出:

复制代码
{"name":"Tom","age":18}

为什么没有 Email

因为:

复制代码
json:"email,omitempty"

Email 是空字符串,omitempty 表示空值不输出。

为什么没有 Password

因为:

复制代码
json:"-"

表示忽略这个字段。

十二、手写一个迷你版 JSON 序列化器

下面我们手写一个简化版 SimpleMarshal

它支持:

  • struct
  • pointer
  • string
  • int
  • bool
  • slice / array
  • mapstringT
  • json:"name"
  • json:"-"
  • omitempty

它不是为了替代标准库,而是为了理解反射在 JSON 序列化里做了什么。

完整代码:

复制代码
package main

import (
	"fmt"
	"reflect"
	"sort"
	"strconv"
	"strings"
)

type User struct {
	Name     string   `json:"name"`
	Age      int      `json:"age"`
	Email    string   `json:"email,omitempty"`
	Password string   `json:"-"`
	Active   bool     `json:"active"`
	Tags     []string `json:"tags,omitempty"`
}

func SimpleMarshal(v any) (string, error) {
	return marshalValue(reflect.ValueOf(v))
}

func marshalValue(v reflect.Value) (string, error) {
	if !v.IsValid() {
		return "null", nil
	}

	if v.Kind() == reflect.Interface {
		if v.IsNil() {
			return "null", nil
		}
		return marshalValue(v.Elem())
	}

	if v.Kind() == reflect.Ptr {
		if v.IsNil() {
			return "null", nil
		}
		return marshalValue(v.Elem())
	}

	switch v.Kind() {
	case reflect.Struct:
		return marshalStruct(v)
	case reflect.String:
		return strconv.Quote(v.String()), nil
	case reflect.Bool:
		if v.Bool() {
			return "true", nil
		}
		return "false", nil
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return strconv.FormatInt(v.Int(), 10), nil
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		return strconv.FormatUint(v.Uint(), 10), nil
	case reflect.Slice, reflect.Array:
		return marshalSlice(v)
	case reflect.Map:
		return marshalMap(v)
	default:
		return "", fmt.Errorf("unsupported kind: %s", v.Kind())
	}
}

func marshalStruct(v reflect.Value) (string, error) {
	t := v.Type()
	parts := make([]string, 0, t.NumField())

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

		// PkgPath 不为空表示非导出字段。
		// 非导出字段不能安全地通过 Interface 读取,JSON 也不会导出它。
		if field.PkgPath != "" {
			continue
		}

		tag := field.Tag.Get("json")
		if tag == "-" {
			continue
		}

		name, omitempty := parseJSONTag(tag)
		if name == "" {
			name = field.Name
		}

		if omitempty && value.IsZero() {
			continue
		}

		encodedValue, err := marshalValue(value)
		if err != nil {
			return "", fmt.Errorf("marshal field %s: %w", field.Name, err)
		}

		parts = append(parts, strconv.Quote(name)+":"+encodedValue)
	}

	return "{" + strings.Join(parts, ",") + "}", nil
}

func marshalSlice(v reflect.Value) (string, error) {
	parts := make([]string, 0, v.Len())

	for i := 0; i < v.Len(); i++ {
		encodedValue, err := marshalValue(v.Index(i))
		if err != nil {
			return "", err
		}
		parts = append(parts, encodedValue)
	}

	return "[" + strings.Join(parts, ",") + "]", nil
}

func marshalMap(v reflect.Value) (string, error) {
	if v.Type().Key().Kind() != reflect.String {
		return "", fmt.Errorf("only map with string keys is supported")
	}

	keys := make([]string, 0, v.Len())
	for _, key := range v.MapKeys() {
		keys = append(keys, key.String())
	}
	sort.Strings(keys)

	parts := make([]string, 0, len(keys))
	for _, key := range keys {
		value := v.MapIndex(reflect.ValueOf(key))

		encodedValue, err := marshalValue(value)
		if err != nil {
			return "", err
		}

		parts = append(parts, strconv.Quote(key)+":"+encodedValue)
	}

	return "{" + strings.Join(parts, ",") + "}", nil
}

func parseJSONTag(tag string) (name string, omitempty bool) {
	if tag == "" {
		return "", false
	}

	items := strings.Split(tag, ",")
	name = items[0]

	for _, item := range items[1:] {
		if item == "omitempty" {
			omitempty = true
		}
	}

	return name, omitempty
}

func main() {
	user := User{
		Name:     "Tom",
		Age:      18,
		Email:    "",
		Password: "secret",
		Active:   true,
		Tags:     []string{"go", "reflect"},
	}

	text, err := SimpleMarshal(user)
	if err != nil {
		fmt.Println("marshal error:", err)
		return
	}

	fmt.Println(text)

	more, err := SimpleMarshal(map[string]any{
		"name": "Jerry",
		"age":  20,
	})
	if err != nil {
		fmt.Println("marshal map error:", err)
		return
	}

	fmt.Println(more)
}

输出:

复制代码
{"name":"Tom","age":18,"active":true,"tags":["go","reflect"]}
{"age":20,"name":"Jerry"}

十三、逐段拆解这个序列化器

入口函数很短:

复制代码
func SimpleMarshal(v any) (string, error) {
	return marshalValue(reflect.ValueOf(v))
}

它把任意值转成 reflect.Value,交给 marshalValue

marshalValue 做的事情是按种类分发:

复制代码
switch v.Kind() {
case reflect.Struct:
	return marshalStruct(v)
case reflect.String:
	return strconv.Quote(v.String()), nil
case reflect.Bool:
	// ...
case reflect.Slice, reflect.Array:
	return marshalSlice(v)
case reflect.Map:
	return marshalMap(v)
}

这就是 JSON 序列化的核心思路:

复制代码
先判断值的种类,再按不同种类编码。

处理指针

复制代码
if v.Kind() == reflect.Ptr {
	if v.IsNil() {
		return "null", nil
	}
	return marshalValue(v.Elem())
}

如果传进来的是:

复制代码
&User{Name: "Tom"}

Kind() 会是:

复制代码
ptr

要用:

复制代码
v.Elem()

拿到指针指向的结构体。

处理结构体

结构体序列化的关键是:

复制代码
t := v.Type()

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

field 是字段元信息:

复制代码
字段名
字段类型
字段 tag
是否导出

value 是字段值:

复制代码
Tom
18
true

读取 tag:

复制代码
tag := field.Tag.Get("json")

忽略字段:

复制代码
if tag == "-" {
	continue
}

解析 omitempty

复制代码
name, omitempty := parseJSONTag(tag)
if omitempty && value.IsZero() {
	continue
}

字段名处理:

复制代码
if name == "" {
	name = field.Name
}

最后拼成:

复制代码
"name":"Tom"

为什么跳过非导出字段

Go 里小写字段是非导出的:

复制代码
type User struct {
	Name string
	age  int
}

标准库 JSON 不会导出小写字段。

反射里可以用:

复制代码
field.PkgPath != ""

判断字段是否非导出。

所以上面的代码写了:

复制代码
if field.PkgPath != "" {
	continue
}

十四、真实 encoding/json 比这个复杂得多

我们写的 SimpleMarshal 只是教学版。

真实的 encoding/json 要处理更多情况:

  • float
  • nil slice
  • nil map
  • 嵌套结构体
  • 匿名字段
  • 字段冲突
  • HTML 转义
  • json.Marshaler
  • encoding.TextMarshaler
  • map key 排序
  • 循环引用检测
  • 更完整的 tag 规则
  • 错误类型和边界情况

所以不要在生产环境使用这个教学版。

它的价值是帮助你理解:

复制代码
JSON 序列化器为什么能处理任意结构体。

原因就是:

复制代码
反射可以在运行时检查值的类型、字段、tag 和字段值。

十五、什么时候不要用反射

如果你明确知道类型,直接写普通代码。

不需要反射:

复制代码
func PrintUser(user User) {
	fmt.Println(user.Name)
}

不需要写成:

复制代码
func PrintUser(user User) {
	v := reflect.ValueOf(user)
	fmt.Println(v.FieldByName("Name"))
}

如果你只是想支持多态,优先用接口:

复制代码
type Writer interface {
	Write([]byte) (int, error)
}

如果你只是关心少数几个类型,优先用类型断言或 type switch。

反射适合:

复制代码
你写的是通用框架或库
你不知道调用方会传什么结构体
你需要读取字段和 tag
你需要按类型动态处理值

十六、新手学习路线

你可以按这个顺序练习反射:

  1. reflect.TypeOf 打印变量类型。
  2. reflect.ValueOf 打印变量值。
  3. 区分 TypeKind
  4. 遍历结构体字段。
  5. 读取 struct tag。
  6. 处理指针:Kind() == reflect.PtrElem()
  7. CanSet 修改一个变量。
  8. 写一个 StructToMap 小工具。
  9. 写一个迷你版 JSON 序列化器。
  10. 再去看 encoding/json 的真实行为。

反射不需要一开始就学得很深。

你先理解:

复制代码
Type 看类型
Value 看值
Kind 看种类
StructField 看字段说明
Tag 看字段标签

就能读懂很多框架代码了。

总结

Go 反射可以概括成几句话:

  • 反射让程序在运行时检查类型和值。
  • reflect.TypeOf 获取类型信息。
  • reflect.ValueOf 获取值信息。
  • Type 是具体类型,Kind 是底层分类。
  • 结构体字段和 struct tag 可以通过反射读取。
  • JSON、ORM、参数校验、配置解析、Web 框架经常使用反射。
  • 类型断言适合已知类型,反射适合未知结构的通用处理。
  • 反射更灵活,但也更复杂、更慢、更容易 panic。
  • encoding/json 能处理任意结构体,本质上就是用反射读取字段、字段值和 json tag。

最后记住一句:

复制代码
反射不是日常业务代码的优先选择,而是编写通用框架和通用工具时的能力。

理解反射之后,你再看 json.Marshal、ORM、validate、配置解析,就不会觉得它们像魔法了。

参考资料