练习
学完本章你应该掌握
- 能区分
reflect.TypeOf得到的具体类型和Kind()得到的类型大类,并处理nil场景。 - 能使用
reflect.ValueOf读取运行时值,并在确认 Kind 后把反射值转回普通值继续处理。 - 能知道为什么反射改值必须传指针、为什么要配合
Elem()、CanSet()、IsValid()使用。 - 能读取结构体字段、tag、方法和返回值,并把反射放进 map 映射、动态分发、表单绑定等小场景中。
- 能识别反射中的典型错误,例如把 tag 当字段名、对错误 Kind 调错方法、方法不存在就直接
Call。 - 能在合适场景下把接口、结构体、map、函数、错误处理等前面学过的知识与反射结合起来,而不是用反射替代一切。
简单
第 1 题:输出具体类型和 Kind
编写一个 Go 程序,完成下面要求:
- 定义自定义类型
LessonID,底层类型为int - 编写函数
showTypeInfo(value any) - 在函数中使用
reflect.TypeOf输出这个值的具体类型和 Kind - 在
main中分别传入:LessonID(16)[]string{"reflect", "tag"}
输出结果应类似这样:
| |
查看参考答案
| |
说明:LessonID 的具体类型是 main.LessonID,但它的 Kind 仍然是 int。这正是 Type 和 Kind 最容易混淆的地方。
第 2 题:用反射安全读取字符串长度
编写函数 stringLength(value any) int,要求如下:
- 在函数中使用
reflect.ValueOf - 如果传入值的 Kind 是
string,返回它的长度 - 如果不是
string,返回-1
在 main 中至少测试下面两次:
stringLength("Go 反射")stringLength(18)
查看参考答案
| |
输出结果:
| |
说明:"Go 反射" 按字节计算长度是 9。这道题真正要练的是先看 Kind,再决定能不能调用 String()。
第 3 题:修正不能修改变量的反射代码
下面程序想把 name 改成 golang,但现在会出错。
请改正代码,让它输出:
| |
| |
查看参考答案
修正后的代码如下:
| |
原因说明:
- 直接把
name传给reflect.ValueOf,拿到的是一个不可设置的值副本 - 要想改原变量,必须传入
&name - 传入指针后,还要用
Elem()取出指针指向的实际变量
第 4 题:安全描述一个可能为 nil 的值
编写函数 describe(value any),要求如下:
- 如果传入的是
nil,输出nil value - 否则输出它的具体类型和 Kind
在 main 中至少测试下面两次:
describe(nil)describe(map[string]int{"go": 16})
查看参考答案
| |
一种可能的输出结果:
| |
说明:reflect.TypeOf(nil) 会直接返回 nil,不能继续立刻调用 Kind()。
第 5 题:遍历结构体字段、tag 和当前值
编写一个 Go 程序,完成下面要求:
- 定义结构体
Course - 字段如下:
| |
- 创建一个
Course值 - 使用反射遍历所有字段
- 逐行输出字段名、json tag、字段 Kind 和字段当前值
输出结果应类似这样:
| |
查看参考答案
| |
说明:这道题把结构体、tag 和反射放到了一起,和前面结构体章节里的 json tag 正好接上。
一般
第 6 题:实现一个可修改整数的反射函数
编写函数 setScore(ptr any, score int) error,要求如下:
- 传入的
ptr必须是一个指针 - 指针指向的值必须是
int,或者底层类型为int的自定义类型 - 如果可以修改,就把它设置成新的分数
- 如果不满足条件,返回错误
在 main 中测试下面这个场景:
| |
如果成功,最后输出:
| |
查看参考答案
| |
说明:Score 虽然是自定义类型,但它的 Kind 仍然是 int,所以这里可以用 SetInt 修改。
第 7 题:按字段名修改结构体字段
编写函数 setFieldByName(dst any, fieldName string, newValue any) error,要求如下:
dst必须是结构体指针- 使用
FieldByName找到目标字段 - 只处理
string和int两种字段 - 字段不存在、不可修改或类型不匹配时返回错误
在 main 中至少完成下面两次修改:
- 把
User.Name改成新名字 - 把
User.Age改成20
查看参考答案
| |
一种可能的输出结果:
| |
说明:这道题会自然复习结构体、字段名、any、类型断言和错误返回。
第 8 题:通过反射调用 Hello 方法
编写函数 callMethod(value any, methodName string, arg string) (string, error),要求如下:
- 使用
MethodByName查找方法 - 方法不存在时返回错误
- 使用
Call调用这个方法 - 返回这个方法的第一个返回值
测试结构体如下:
| |
在 main 中调用:
| |
期望结果:
| |
查看参考答案
| |
说明:Call 的参数和返回值都是 []reflect.Value,这和普通方法调用很不一样,也是反射代码最容易写错的地方之一。
第 9 题:修正把 json tag 当字段名查找的代码
下面程序想修改 User.Name,但它现在找不到字段。
请改正代码,并说明原因。
| |
查看参考答案
修正后的代码如下:
| |
原因说明:
FieldByName查找的是结构体真实字段名,不是 tag 名- 这里真实字段名是
Name,而json:"name"只是字段元信息 - 如果想按 tag 查找,必须先遍历字段,再自己读取和比较 tag
第 10 题:实现支持结构体指针的 ToMap
编写函数 ToMap(value any) (map[string]any, error),要求如下:
- 支持传入结构体值或结构体指针
- 如果字段有
dbtag,就优先用dbtag 作为键 - 如果没有
dbtag,就使用字段名 - 如果
db:"-",跳过这个字段 - 如果传入的不是结构体或结构体指针,返回错误
测试结构体如下:
| |
在 main 中请测试:
| |
查看参考答案
| |
一种可能的输出结果:
| |
说明:map 的输出顺序不固定,只要键值正确即可。这道题顺手复习了上一章的 tag,也把本章的简单映射示例往前推了一步。
进阶
下面四题会更接近真实工具函数里的反射用法。
做题过程中,你会自然用到前面学过的结构体、map、切片、函数、接口、错误处理和字符串转整数。
第 11 题:按 tag 把 map 绑定到结构体
编写函数 BindFromMap(dst any, data map[string]string) error,要求如下:
dst必须是结构体指针- 根据字段上的
formtag 找到对应的输入值 - 只处理
string和int两种字段 int字段需要把字符串转换成整数- 转换失败时返回错误
测试结构体如下:
| |
测试数据如下:
| |
查看参考答案
| |
一种可能的输出结果:
| |
说明:这已经很接近表单绑定、配置加载这类真实场景了。这里额外用到了 strconv.Atoi,因为反射能找到字段,但字符串到整数的转换仍然要我们自己做。
第 12 题:按方法名批量调用不同结构体
编写函数 callAll(list []any, methodName string, arg string) []string,要求如下:
- 遍历切片中的每个值
- 使用反射按方法名调用方法
- 如果某个值没有这个方法,就在结果里追加
skip - 如果有这个方法,就把返回结果追加到结果切片里
测试类型如下:
| |
测试切片如下:
| |
期望结果应类似这样:
| |
查看参考答案
| |
一种可能的输出结果:
| |
说明:这道题把反射方法调用、切片遍历和 any 放在了一起。能不能做这件事,不等于平时业务里就一定应该这样做,它更适合动态分发场景。
第 13 题:实现一个简化的插入字段构造器
请实现函数 buildInsertParts(value any) (fields []string, values []any, err error),要求如下:
- 支持结构体值或结构体指针
- 读取字段上的
dbtag - 如果
db:"-",跳过这个字段 - 按结构体字段原始顺序返回
fields和values
测试结构体如下:
| |
调用:
| |
期望结果应类似这样:
| |
查看参考答案
| |
输出结果:
| |
说明:这道题比 ToMap 更接近 ORM 里“按字段顺序组织 SQL 参数”的思路,也会自然复习切片追加和结构体 tag。
第 14 题:先走接口校验,再做反射映射
请完成一个综合练习:
- 定义接口
Validator - 约定它只有一个方法:
Validate() error - 定义结构体
User:
| |
- 给
User实现Validate() error - 当
Name == ""时返回错误 - 当
Age <= 0时返回错误 - 编写函数
BuildPayload(value any) (map[string]any, error) BuildPayload需要先检查值是否实现了Validator- 校验通过后,再使用反射读取结构体字段和
dbtag - 返回一个
map[string]any - 同时支持结构体值和结构体指针
在 main 中至少测试两个场景:
- 一个合法用户
- 一个非法用户
查看参考答案
| |
一种可能的输出结果:
| |
说明:这道题刻意把“接口校验”和“反射映射”放在一起,是为了练习本章最后一节提到的思路。能先用接口表达清楚的业务规则,就不要一上来全交给反射。