news 2026/7/3 6:05:25

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Go 反射入门:概念、应用场景与 JSON 序列化原理

我们聊一个问题:

为什么 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 可能读取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要在运行时读取结构体字段、字段值和jsontag。

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"` }

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

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
  • map[string]T
  • 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能处理任意结构体,本质上就是用反射读取字段、字段值和jsontag。

最后记住一句:

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

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

参考资料

  • Package reflect
  • The Laws of Reflection
  • Package encoding/json
  • Go Wiki: Well-known struct tags
  • Package encoding
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/3 6:04:30

机器人软件开发主流编程语言全场景选型指南:分工业 / 服务 / 移动机器人落地标准

前言 很多刚入行的机器人研发工程师、在校相关专业学生、自动化项目从业者都会陷入同一个核心困惑:到底哪一门编程语言才是机器人开发最优解?网上各类碎片化教程各执一词,有人推崇底层高性能语言,有人认为脚本语言入门更快,不同行业从业者给出的答案截然不同。工业机械臂…

作者头像 李华
网站建设 2026/7/3 6:04:06

自动驾驶卡车技术栈与商业落地:重构货运经济的新引擎

1. 项目概述&#xff1a;当卡车自己跑起来“Autonomous Trucks and the New Freight Economy”——自动驾驶卡车与新货运经济。这不仅仅是一个技术话题&#xff0c;更是一场正在我们身边发生的、静默但深刻的产业革命。作为一名长期关注物流技术与供应链变革的从业者&#xff0…

作者头像 李华
网站建设 2026/7/3 6:02:09

江苏省工程技术研究中心认定对企业有什么好处?如何申报

一、江苏省工程技术研究中心认定好处获得该资质意味着企业打通了“政策资金税收优惠项目申报”的绿色通道&#xff1a;1.直接资金奖励省级奖励&#xff1a;根据2026年江苏省最新政策&#xff0c;认定为省级工程技术研究中心&#xff0c;省级财政给予最高100万元的直接奖励。地方…

作者头像 李华
网站建设 2026/7/3 6:02:06

Linux 系统编程 09:线程基础

前言&#xff1a;承接上一篇 System V IPC 三大进程间通信机制&#xff0c;多进程模型实现了任务并发&#xff0c;但进程间切换开销大、通信成本高&#xff0c;在高频并发场景下并非最优解。本篇引入更轻量的并发执行单元 —— 线程&#xff0c;讲解 Linux 线程的底层本质、POS…

作者头像 李华
网站建设 2026/7/3 6:02:02

SPECTROLAB S光谱仪火花激发异常故障排查与解决方案

SPECTROLAB S直读光谱仪火花激发异常是设备高频故障&#xff0c;主要表现为无火花、火花微弱、火花偏斜、连续拉弧、激发报错&#xff08;No Spark、Source Off、Clamp Up、Argon Low&#xff09;、激发斑点发黑/不规则等&#xff0c;直接导致样品无法激发、数据漂移、检测失效…

作者头像 李华
网站建设 2026/7/3 6:00:18

MySQL索引完整教程:创建、查看、修改、删除与日常管理

一、索引基础说明 InnoDB 支持&#xff1a;主键索引、普通索引、唯一索引、联合复合索引、前缀索引、全文索引&#xff1b; 索引核心作用&#xff1a;加速 WHERE / JOIN / ORDER BY / GROUP BY 查询&#xff1b; 代价&#xff1a;插入、更新、删除时需要维护 B 树&#xff0c;索…

作者头像 李华