Go 从 0 到精通 · 第 35 课:反射入门

学习定位:这是整套 Go 教程的第 35 课,也是阶段六(高级进阶阶段)的第二课。
前置要求:已经完成第 34 课,理解 Go 的类型系统、接口、结构体、方法和泛型基础。
本课目标:理解 Go 反射的核心作用,掌握 reflect.Typereflect.ValueKindElemCanSet 等关键概念,学会在运行时读取类型信息、遍历结构体字段、读取标签、修改可设置值,并知道反射的代价、适用场景和常见坑。


1. 本课你要解决的核心问题

前面的 Go 代码,大多数都是“编译期就知道类型”的。

例如:

  • var n int = 10
  • user.Name
  • tasks[0].Title

编译器在你写代码的时候,就已经知道:

  • 变量是什么类型
  • 有哪些字段
  • 有哪些方法

但真实项目里,有一类问题在编译期并不知道全部信息:

  • 传进来的到底是不是结构体
  • 这个结构体有哪些字段
  • 字段上有没有 json:"name" 这种标签
  • 我想根据字符串 "Name" 动态找到字段并赋值
  • 我写的是通用框架代码,不知道用户会传什么类型

这时候,普通静态写法就不够了。

反射解决的问题就是:让程序在运行时查看和操作类型信息。

这听起来很强,但也很危险,因为它会带来:

  • 代码更绕
  • 性能更差
  • 出错更隐蔽

所以这节课要讲清楚两件事:

  1. 反射到底能做什么
  2. 为什么它必须谨慎使用

2. 先建立直觉:什么叫“运行时看类型”

看一个最简单的例子:

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 x int = 42

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

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

这里做了什么?

  • reflect.TypeOf(x):拿到变量的类型描述
  • reflect.ValueOf(x):拿到变量当前的值描述

也就是说,反射把“类型”和“值”都包装成了可在运行时分析的对象。

你可以先把它记成:

  • Type 关注“它是什么”
  • Value 关注“它现在的值是什么”

3. reflect.Typereflect.Value 是两块核心地基

3.1 reflect.Type

reflect.Type 用来描述类型信息。

常见操作:

  • 看类型名
  • 看种类
  • 看字段
  • 看方法

例如:

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

func main() {
u := User{Name: "张三", Age: 18}
t := reflect.TypeOf(u)

fmt.Println(t.Name()) // User
fmt.Println(t.Kind()) // struct
fmt.Println(t.NumField()) // 2
}

3.2 reflect.Value

reflect.Value 用来描述值。

常见操作:

  • 读取值
  • 判断值类型
  • 取字段值
  • 在满足条件时修改值

例如:

1
2
3
4
5
6
u := User{Name: "张三", Age: 18}
v := reflect.ValueOf(u)

fmt.Println(v.Kind()) // struct
fmt.Println(v.FieldByName("Name")) // 张三
fmt.Println(v.FieldByName("Age").Int()) // 18

3.3 一句话区分

你可以先这样记:

  • Type 更像“说明书”
  • Value 更像“当前实例”

很多反射代码,本质上就是在 TypeValue 之间来回切换。


4. Type 不等于 Kind

这是反射里第一个很重要、也很容易混的点。

4.1 Type 是具体类型

例如:

1
2
type MyInt int
type User struct{}

它们的 Type 分别是:

  • MyInt
  • User

4.2 Kind 是底层类别

例如:

  • int
  • string
  • struct
  • slice
  • map
  • ptr

看例子:

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

import (
"fmt"
"reflect"
)

type MyInt int

func main() {
var x MyInt = 10

t := reflect.TypeOf(x)
fmt.Println(t.Name()) // MyInt
fmt.Println(t.Kind()) // int
}

这里:

  • Name()MyInt
  • Kind()int

4.3 为什么要区分这两个

因为很多反射逻辑判断的是“它属于哪一类”。

比如:

  • 如果 Kind() == reflect.Struct,那就可以遍历字段
  • 如果 Kind() == reflect.Slice,那就可以遍历元素
  • 如果 Kind() == reflect.Ptr,那就应该先 Elem()

所以:

  • 想知道具体类型是谁,看 Type
  • 想知道应该怎么处理它,看 Kind

5. 读取基本类型信息

下面先把最常见的几个 API 过一遍。

5.1 读取类型名和种类

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

import (
"fmt"
"reflect"
)

type User struct {
Name string
}

func main() {
u := User{Name: "李四"}
t := reflect.TypeOf(u)

fmt.Println("类型名:", t.Name()) // User
fmt.Println("种类:", t.Kind()) // struct
fmt.Println("完整类型:", t.String()) // main.User
}

5.2 判断是不是某种类别

1
2
3
4
5
6
7
8
9
10
11
12
13
func PrintKind(x any) {
t := reflect.TypeOf(x)
switch t.Kind() {
case reflect.Int:
fmt.Println("这是整数")
case reflect.String:
fmt.Println("这是字符串")
case reflect.Struct:
fmt.Println("这是结构体")
default:
fmt.Println("其他类型")
}
}

5.3 读取值

1
2
3
4
func PrintValue(x any) {
v := reflect.ValueOf(x)
fmt.Println("值:", v)
}

不过要注意:

reflect.Value 不是普通值本身,而是一个“反射包装对象”。你通常还要调用它的方法拿到具体内容。

例如:

1
2
v := reflect.ValueOf(123)
fmt.Println(v.Int()) // 123

6. 不同 Kind 取值方式不同

这是反射里最容易踩坑的一类问题。

6.1 整数用 Int()

1
2
v := reflect.ValueOf(123)
fmt.Println(v.Int())

6.2 字符串用 String()

1
2
v := reflect.ValueOf("hello")
fmt.Println(v.String())

6.3 布尔值用 Bool()

1
2
v := reflect.ValueOf(true)
fmt.Println(v.Bool())

6.4 浮点数用 Float()

1
2
v := reflect.ValueOf(3.14)
fmt.Println(v.Float())

6.5 错误示例

1
2
v := reflect.ValueOf("hello")
fmt.Println(v.Int()) // panic

为什么?

因为 v 的底层种类是 string,你却按整数去取。

所以反射里一个非常重要的习惯是:

先看 Kind(),再决定调用哪个取值方法。


7. 指针和 Elem():反射里最关键的入口之一

很多反射代码看着复杂,本质都绕不开 Elem()

7.1 什么是 Elem()

如果一个值是:

  • 指针
  • 接口
  • 切片元素
  • 数组元素

那你经常需要“进入下一层”。

对于指针来说,Elem() 表示“指针指向的那个值”。

7.2 先看一个例子

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

import (
"fmt"
"reflect"
)

func main() {
x := 10

vp := reflect.ValueOf(&x)
fmt.Println(vp.Kind()) // ptr

ve := vp.Elem()
fmt.Println(ve.Kind()) // int
fmt.Println(ve.Int()) // 10
}

这里:

  • &xKindptr
  • vp.Elem() 才是它指向的那个 int

7.3 为什么 Elem() 这么重要

因为很多时候你想修改原值,必须拿到它指向的真实对象,而不是那个指针包装。

例如修改变量:

1
2
3
4
x := 10
v := reflect.ValueOf(&x).Elem()
v.SetInt(99)
fmt.Println(x) // 99

如果你不传指针,而是直接:

1
2
v := reflect.ValueOf(x)
v.SetInt(99)

会直接 panic,因为它不是可设置的值。


8. 修改值:CanSet 是关键判断

反射不仅能看,还能改,但不是所有值都能改。

8.1 先看失败例子

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

import (
"fmt"
"reflect"
)

func main() {
x := 10
v := reflect.ValueOf(x)

fmt.Println(v.CanSet()) // false
// v.SetInt(20) // panic
}

为什么不能改?

因为 reflect.ValueOf(x) 拿到的是 x 的一个副本信息,不是可回写的原对象。

8.2 正确写法:传指针,再 Elem()

1
2
3
4
5
6
7
8
func main() {
x := 10
v := reflect.ValueOf(&x).Elem()

fmt.Println(v.CanSet()) // true
v.SetInt(20)
fmt.Println(x) // 20
}

8.3 一个通用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func SetIntValue(ptr any, newValue int64) error {
v := reflect.ValueOf(ptr)

if v.Kind() != reflect.Ptr {
return fmt.Errorf("需要传入指针")
}

elem := v.Elem()
if elem.Kind() != reflect.Int {
return fmt.Errorf("需要指向 int 的指针")
}

if !elem.CanSet() {
return fmt.Errorf("目标不可修改")
}

elem.SetInt(newValue)
return nil
}

调用:

1
2
3
var x int = 10
err := SetIntValue(&x, 99)
fmt.Println(x, err) // 99 <nil>

8.4 一个经验口诀

想通过反射修改值,通常要满足三件事:

  1. 传的是指针
  2. 找到了正确的目标值
  3. CanSet()true

9. 结构体反射:最常见、最实用

反射在实际项目里最常见的对象不是基本类型,而是结构体。

因为很多框架都需要:

  • 看结构体字段
  • 看字段标签
  • 按字段名读写

9.1 遍历结构体字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
"reflect"
)

type User struct {
Name string
Age int
}

func main() {
u := User{Name: "王五", Age: 20}

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)
fmt.Println("字段类型:", fieldType.Type)
fmt.Println("字段值:", fieldValue)
}
}

输出大致会是:

1
2
3
4
5
6
字段名: Name
字段类型: string
字段值: 王五
字段名: Age
字段类型: int
字段值: 20

9.2 按字段名查找

1
2
3
4
5
u := User{Name: "王五", Age: 20}
v := reflect.ValueOf(u)

fmt.Println(v.FieldByName("Name")) // 王五
fmt.Println(v.FieldByName("Age")) // 20

如果字段不存在:

1
2
f := v.FieldByName("Email")
fmt.Println(f.IsValid()) // false

所以按名字找字段时,最好先判断 IsValid()


10. 读取结构体标签:这就是很多框架的基础

很多标准库和框架都依赖标签,例如:

1
2
3
4
type User struct {
Name string `json:"name" db:"user_name"`
Age int `json:"age" db:"user_age"`
}

反射可以读这些标签。

10.1 读取标签示例

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

import (
"fmt"
"reflect"
)

type User struct {
Name string `json:"name" db:"user_name"`
Age int `json:"age" db:"user_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("db 标签:", field.Tag.Get("db"))
}
}

10.2 为什么这很重要

像这些能力背后大多依赖反射:

  • encoding/json 根据 json 标签做编解码
  • ORM 根据 db 标签做字段映射
  • 参数校验库根据 validate 标签做规则验证

所以学反射,不只是为了自己手写反射代码,更重要的是:

你开始能读懂这些库为什么能“自动工作”。


11. 修改结构体字段:必须满足条件

修改结构体字段是反射里第二个常见操作,但限制也更多。

11.1 正确示例

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

import (
"fmt"
"reflect"
)

type User struct {
Name string
Age int
}

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

v := reflect.ValueOf(&u).Elem()
nameField := v.FieldByName("Name")

fmt.Println(nameField.CanSet()) // true
nameField.SetString("李四")

fmt.Println(u.Name) // 李四
}

11.2 为什么一定要 &u

因为:

  • reflect.ValueOf(u) 拿到的是值副本
  • reflect.ValueOf(&u).Elem() 才是原结构体本身

11.3 必须是导出字段

如果结构体字段是小写:

1
2
3
type User struct {
name string
}

那么即使你拿到了结构体值,反射也不能随便改这个未导出字段。

这也是 Go 可见性规则在反射层面的延续。


12. 一个完整实战:打印结构体信息

下面写一个很典型的小工具函数:接收任意结构体,打印字段名、类型、值和标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
"fmt"
"reflect"
)

type User struct {
Name string `json:"name"`
Age int `json:"age"`
}

func InspectStruct(x any) {
t := reflect.TypeOf(x)
v := reflect.ValueOf(x)

if t.Kind() == reflect.Ptr {
t = t.Elem()
v = v.Elem()
}

if t.Kind() != reflect.Struct {
fmt.Println("传入的不是结构体")
return
}

fmt.Println("结构体类型:", t.Name())

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

fmt.Printf("字段: %s, 类型: %s, 值: %v, json标签: %q\n",
fieldType.Name,
fieldType.Type,
fieldValue.Interface(),
fieldType.Tag.Get("json"),
)
}
}

func main() {
u := User{Name: "赵六", Age: 28}
InspectStruct(u)
}

12.1 这个例子体现了哪些关键点

它把这节课的几个主干串起来了:

  • TypeOf / ValueOf 拿类型和值
  • Kind() 判断是不是指针、是不是结构体
  • Elem() 解引用
  • NumField()Field(i) 遍历字段
  • Tag.Get() 读取标签
  • Interface() 拿到可打印的普通值

如果你能把这个函数读顺,说明你已经真正入门反射了。


13. Interface() 是把反射值还原回普通值

这个方法经常出现在反射代码里。

例如:

1
2
v := reflect.ValueOf(123)
fmt.Println(v.Interface()) // 123

它的作用可以理解为:

reflect.Value 里的内容重新取出来,作为一个普通的 interface{} 值返回

这样你就能:

  • 交给 fmt.Println
  • 做类型断言
  • 传给其他普通函数

例如:

1
2
3
4
5
v := reflect.ValueOf("hello")
value := v.Interface()

s, ok := value.(string)
fmt.Println(s, ok) // hello true

所以很多反射流程最后都会回到:

1
fieldValue.Interface()

14. 反射和接口的关系

这部分必须讲清楚,否则很多反射代码会看得一头雾水。

14.1 反射常常从 any 开始

很多通用函数写成:

1
func Inspect(x any) { ... }

为什么?

因为调用者可能传:

  • int
  • string
  • User
  • *User

any 可以接住所有类型。

14.2 反射的本质,是把接口里的动态类型拿出来

当一个值被放进接口后,运行时其实仍然记得:

  • 它的真实类型是什么
  • 它的真实值是什么

reflect.TypeOf(x)reflect.ValueOf(x) 就是在把这些信息重新拿出来。

所以你可以把反射理解成:

“对接口中真实类型信息的运行时访问能力”

这个理解非常重要。


15. 反射能做什么,真实项目里为什么常见

很多人第一次学反射会觉得:“这玩意我平时好像不用啊。”

其实你经常在“间接使用”它。

15.1 编解码

例如 encoding/json

  • 看结构体字段
  • json 标签
  • 把 JSON 数据填进对应字段

15.2 ORM / 数据库映射

例如很多 ORM 会:

  • 看结构体字段名
  • 看数据库标签
  • 自动构造扫描和映射逻辑

15.3 配置加载

有些配置库会:

  • 读取环境变量或配置文件
  • 根据标签把值填充到结构体

15.4 参数校验

有些校验库会:

  • 遍历结构体字段
  • 读取 validate:"required" 之类的标签
  • 按规则做校验

15.5 框架能力

很多框架的“自动注册”“自动绑定”“自动注入”背后,本质都是反射。

所以反射是一个“底层基础设施工具”。

业务代码里你可能不常直接写它,但你会大量遇到它的结果。


16. 为什么说反射要慎用

这节是整课里最重要的价值判断部分。

16.1 可读性差

普通代码一眼就知道在干什么:

1
user.Name = "张三"

反射代码则是:

1
2
3
v := reflect.ValueOf(&user).Elem()
f := v.FieldByName("Name")
f.SetString("张三")

显然更绕。

16.2 容易 panic

例如:

  • stringInt()
  • 对不可设置值调用 Set
  • 对非指针调用 Elem()
  • 字段不存在却继续操作

这些都很容易在运行时直接 panic。

16.3 性能更差

反射需要:

  • 做动态类型检查
  • 做额外包装
  • 走运行时分派逻辑

这通常比直接访问字段、直接调用函数更慢。

16.4 编译器帮不了太多

普通代码很多错误能在编译期发现。

反射代码则有一大类错误只能等到运行时暴露。

所以反射的原则很简单:

能不用就不用,必须用时把范围收窄。


17. 泛型出现后,哪些场景不必再用反射

这是你现在这个学习阶段很值得建立的判断力。

在 Go 引入泛型之前,很多“通用处理”只能在:

  • 重复代码
  • 反射
  • interface{} + 类型断言

之间选。

现在多了一个更好的选项:泛型。

17.1 例如切片工具函数

过去有人会写各种“反射版通用切片处理”。

现在像这种场景:

1
func Contains[T comparable](items []T, target T) bool

显然泛型比反射更合适。

17.2 那反射还剩什么价值

反射仍然适合:

  • 处理结构体标签
  • 处理运行时未知结构
  • 写通用框架和基础库
  • 编解码、绑定、注入、校验这类动态机制

所以一个实用判断是:

  • “同一算法适用于多种已知类型” -> 更像泛型
  • “运行时才知道结构和字段” -> 更像反射

18. 常见坑总结

18.1 忘了判断 Kind

例如:

1
2
3
4
func PrintInt(x any) {
v := reflect.ValueOf(x)
fmt.Println(v.Int())
}

如果传进来不是整数,就会 panic。

正确思路:

  • 先判断 v.Kind()
  • 再做对应操作

18.2 对非指针调用 Elem()

1
2
v := reflect.ValueOf(10)
fmt.Println(v.Elem()) // panic

Elem() 不是随便都能调,至少得先确认它是指针或接口等可解开的值。

18.3 以为拿到 Value 就一定能改

1
2
v := reflect.ValueOf(10)
fmt.Println(v.CanSet()) // false

只有可寻址、可设置的值才能改,通常需要:

1
reflect.ValueOf(&x).Elem()

18.4 忘了字段可能不存在

1
2
f := v.FieldByName("Email")
f.SetString("a@b.com") // 可能 panic

应该先判断:

1
2
3
if !f.IsValid() {
return
}

18.5 忘了字段可能不可设置

即使字段存在,也不代表可以改。

要先看:

1
f.CanSet()

18.6 在普通业务逻辑里滥用反射

例如你明明知道是:

1
user.Name

却还写成:

1
reflect.ValueOf(&user).Elem().FieldByName("Name")

这基本只会让代码更差。

18.7 反射代码不做错误保护

反射比普通代码更应该:

  • 提前判断类型
  • 提前判断字段是否存在
  • 提前判断是否可设置
  • 在必要处返回错误而不是直接 panic

19. 一个更实用的安全版本:按字段名设置字符串

下面写一个更接近真实工具函数的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package main

import (
"fmt"
"reflect"
)

type User struct {
Name string
City string
}

func SetStringField(ptr any, fieldName string, newValue string) error {
v := reflect.ValueOf(ptr)
if v.Kind() != reflect.Ptr {
return fmt.Errorf("需要传入结构体指针")
}

elem := v.Elem()
if elem.Kind() != reflect.Struct {
return fmt.Errorf("需要传入指向结构体的指针")
}

field := elem.FieldByName(fieldName)
if !field.IsValid() {
return fmt.Errorf("字段 %s 不存在", fieldName)
}

if field.Kind() != reflect.String {
return fmt.Errorf("字段 %s 不是 string 类型", fieldName)
}

if !field.CanSet() {
return fmt.Errorf("字段 %s 不可设置", fieldName)
}

field.SetString(newValue)
return nil
}

func main() {
u := User{Name: "张三", City: "北京"}

err := SetStringField(&u, "City", "上海")
fmt.Println(err) // <nil>
fmt.Println(u) // {张三 上海}
}

19.1 这个例子为什么比直接 panic 更好

因为它在每一步都做了边界检查:

  • 是不是指针
  • 指向的是不是结构体
  • 字段存不存在
  • 字段类型对不对
  • 字段能不能改

这就是反射代码和普通代码最大的写法差别:

你必须更防御式。


20. 本课练习

练习 1:打印变量类型与值

写一个函数:

1
func Describe(x any)

要求打印:

  • 具体类型
  • Kind
  • 当前值

并分别测试:

  • int
  • string
  • []int
  • 结构体

练习 2:遍历结构体字段

定义一个 Book 结构体,包含:

  • Title
  • Author
  • Price

写一个函数,使用反射打印每个字段的:

  • 字段名
  • 字段类型
  • 字段值

练习 3:读取标签

Book 增加标签:

1
2
3
4
5
type Book struct {
Title string `json:"title" db:"title"`
Author string `json:"author" db:"author"`
Price float64 `json:"price" db:"price"`
}

写一个函数读取并打印每个字段的 jsondb 标签。


练习 4:修改字段值

写一个函数:

1
func SetIntField(ptr any, fieldName string, value int64) error

要求:

  • 只允许修改 int 类型字段
  • 如果字段不存在、不是 int、不可设置,都返回错误

练习 5:对比反射与普通写法

分别实现下面两种逻辑:

  1. 直接把 user.Name 改成 "李四"
  2. 用反射把字段 "Name" 改成 "李四"

然后比较:

  • 哪个更简单
  • 哪个更安全
  • 哪个更适合普通业务代码

21. 自测题

21.1 概念题

  1. Go 反射主要解决什么问题?
  2. reflect.Typereflect.Value 分别表示什么?
  3. TypeKind 有什么区别?
  4. 为什么很多反射代码都要先判断 Kind()
  5. Elem() 在什么场景下最常见?它的作用是什么?
  6. 为什么通过反射修改值时通常要传指针?
  7. CanSet() 的意义是什么?
  8. 为什么说反射更适合框架和基础库,而不适合滥用在普通业务代码里?

21.2 代码阅读题

下面这段代码有两个明显问题,试着找出来:

1
2
3
4
5
func SetName(x any) {
v := reflect.ValueOf(x)
field := v.FieldByName("Name")
field.SetString("李四")
}
点击查看答案

问题 1:没有传指针,也没有 Elem()

  • reflect.ValueOf(x) 如果拿到的是结构体值副本,字段通常不可设置
  • 正确做法通常是传入结构体指针,然后 Elem() 取到原结构体

问题 2:没有做任何安全检查。

  • 没判断 x 是不是结构体或结构体指针
  • 没判断字段 "Name" 是否存在
  • 没判断字段是不是 string
  • 没判断字段是否可设置

更稳妥的写法应该像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func SetName(x any) error {
v := reflect.ValueOf(x)
if v.Kind() != reflect.Ptr {
return fmt.Errorf("需要传入指针")
}

elem := v.Elem()
if elem.Kind() != reflect.Struct {
return fmt.Errorf("需要传入结构体指针")
}

field := elem.FieldByName("Name")
if !field.IsValid() {
return fmt.Errorf("字段 Name 不存在")
}
if field.Kind() != reflect.String {
return fmt.Errorf("字段 Name 不是 string 类型")
}
if !field.CanSet() {
return fmt.Errorf("字段 Name 不可设置")
}

field.SetString("李四")
return nil
}

22. 本课总结

这一课你已经进入 Go 里最“动态”的能力之一。

知识点 要点
反射目标 在运行时查看和操作类型与值
两大核心 reflect.Type 看类型,reflect.Value 看值
关键判断 Kind() 决定当前值该怎么处理
关键入口 Elem() 用来进入指针或接口内部值
修改前提 指针、正确目标、CanSet() 为真
典型用途 标签读取、编解码、框架能力、动态绑定

最重要的四件事:

  1. 反射解决的是“运行时才知道结构”的问题
  2. 反射代码一定要先做类型和边界检查
  3. 很多框架大量使用反射,但普通业务代码不该为了炫技滥用它
  4. 泛型解决的是通用类型复用,反射解决的是运行时动态检查,两者不是一回事

23. 下一课预告

你已经学完泛型和反射,接下来我们要转向另一个非常重要的高级主题:Go 的内存行为。

下一课:内存与逃逸分析基础

会重点讲:

  • 栈和堆的基本区别
  • 什么是逃逸分析
  • 哪些写法容易导致变量逃逸到堆上
  • 值传递、指针传递和内存分配之间的关系
  • 怎么建立对性能和内存行为的初步判断

学完下一课,你会开始真正理解“为什么这段 Go 代码会有这样的性能表现”。