我们聊一个问题:
为什么 json.Marshal 可以接收任意结构体?
比如:
json.Marshal(User{})
json.Marshal(Product{})
json.Marshal([]Order{})
json.Marshal 在编译时并不知道你会传什么类型。
但是它运行时却能知道:
- 你传进来的是不是结构体
- 结构体有几个字段
- 字段叫什么
- 字段值是多少
- 字段上有没有
jsontag - 哪些字段要忽略
- 哪些字段空值不输出
这背后靠的就是 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 可能读取
db、gorm - 参数校验库可能读取
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.Marshalerencoding.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
你需要按类型动态处理值
十六、新手学习路线
你可以按这个顺序练习反射:
- 用
reflect.TypeOf打印变量类型。 - 用
reflect.ValueOf打印变量值。 - 区分
Type和Kind。 - 遍历结构体字段。
- 读取 struct tag。
- 处理指针:
Kind() == reflect.Ptr和Elem()。 - 用
CanSet修改一个变量。 - 写一个
StructToMap小工具。 - 写一个迷你版 JSON 序列化器。
- 再去看
encoding/json的真实行为。
反射不需要一开始就学得很深。
你先理解:
Type 看类型
Value 看值
Kind 看种类
StructField 看字段说明
Tag 看字段标签
就能读懂很多框架代码了。
总结
Go 反射可以概括成几句话:
- 反射让程序在运行时检查类型和值。
reflect.TypeOf获取类型信息。reflect.ValueOf获取值信息。Type是具体类型,Kind是底层分类。- 结构体字段和 struct tag 可以通过反射读取。
- JSON、ORM、参数校验、配置解析、Web 框架经常使用反射。
- 类型断言适合已知类型,反射适合未知结构的通用处理。
- 反射更灵活,但也更复杂、更慢、更容易 panic。
encoding/json能处理任意结构体,本质上就是用反射读取字段、字段值和jsontag。
最后记住一句:
反射不是日常业务代码的优先选择,而是编写通用框架和通用工具时的能力。
理解反射之后,你再看 json.Marshal、ORM、validate、配置解析,就不会觉得它们像魔法了。