章节

16.9 练习

本篇通过若干分级练习巩固反射中的类型判断、取值、改值、结构体与方法处理,并达到编写基础反射工具函数的能力。

练习

学完本章你应该掌握

  • 能区分 reflect.TypeOf 得到的具体类型和 Kind() 得到的类型大类,并处理 nil 场景。
  • 能使用 reflect.ValueOf 读取运行时值,并在确认 Kind 后把反射值转回普通值继续处理。
  • 能知道为什么反射改值必须传指针、为什么要配合 Elem()CanSet()IsValid() 使用。
  • 能读取结构体字段、tag、方法和返回值,并把反射放进 map 映射、动态分发、表单绑定等小场景中。
  • 能识别反射中的典型错误,例如把 tag 当字段名、对错误 Kind 调错方法、方法不存在就直接 Call
  • 能在合适场景下把接口、结构体、map、函数、错误处理等前面学过的知识与反射结合起来,而不是用反射替代一切。

简单

第 1 题:输出具体类型和 Kind

第 1 题 简单

编写一个 Go 程序,完成下面要求:

  • 定义自定义类型 LessonID,底层类型为 int
  • 编写函数 showTypeInfo(value any)
  • 在函数中使用 reflect.TypeOf 输出这个值的具体类型和 Kind
  • main 中分别传入: LessonID(16) []string{"reflect", "tag"}

输出结果应类似这样:

1
2
type=main.LessonID kind=int
type=[]string kind=slice
查看参考答案
 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 LessonID int

func showTypeInfo(value any) {
	typ := reflect.TypeOf(value)
	if typ == nil {
		fmt.Println("type=<nil> kind=<nil>")
		return
	}

	fmt.Printf("type=%v kind=%v\n", typ, typ.Kind())
}

func main() {
	showTypeInfo(LessonID(16))
	showTypeInfo([]string{"reflect", "tag"})
}

说明:LessonID 的具体类型是 main.LessonID,但它的 Kind 仍然是 int。这正是 TypeKind 最容易混淆的地方。

第 2 题:用反射安全读取字符串长度

第 2 题 简单

编写函数 stringLength(value any) int,要求如下:

  • 在函数中使用 reflect.ValueOf
  • 如果传入值的 Kind 是 string,返回它的长度
  • 如果不是 string,返回 -1

main 中至少测试下面两次:

  • stringLength("Go 反射")
  • stringLength(18)
查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
	"fmt"
	"reflect"
)

func stringLength(value any) int {
	v := reflect.ValueOf(value)
	if !v.IsValid() {
		return -1
	}

	if v.Kind() != reflect.String {
		return -1
	}

	return len(v.String())
}

func main() {
	fmt.Println(stringLength("Go 反射"))
	fmt.Println(stringLength(18))
}

输出结果:

1
2
9
-1

说明:"Go 反射" 按字节计算长度是 9。这道题真正要练的是先看 Kind,再决定能不能调用 String()

第 3 题:修正不能修改变量的反射代码

第 3 题 简单

下面程序想把 name 改成 golang,但现在会出错。
请改正代码,让它输出:

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

import (
	"fmt"
	"reflect"
)

func main() {
	name := "go"

	value := reflect.ValueOf(name)
	value.SetString("golang")

	fmt.Println(name)
}
查看参考答案

修正后的代码如下:

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

import (
	"fmt"
	"reflect"
)

func main() {
	name := "go"

	value := reflect.ValueOf(&name).Elem()
	if value.CanSet() {
		value.SetString("golang")
	}

	fmt.Println(name)
}

原因说明:

  • 直接把 name 传给 reflect.ValueOf,拿到的是一个不可设置的值副本
  • 要想改原变量,必须传入 &name
  • 传入指针后,还要用 Elem() 取出指针指向的实际变量

第 4 题:安全描述一个可能为 nil 的值

第 4 题 简单

编写函数 describe(value any),要求如下:

  • 如果传入的是 nil,输出 nil value
  • 否则输出它的具体类型和 Kind

main 中至少测试下面两次:

  • describe(nil)
  • describe(map[string]int{"go": 16})
查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
	"fmt"
	"reflect"
)

func describe(value any) {
	typ := reflect.TypeOf(value)
	if typ == nil {
		fmt.Println("nil value")
		return
	}

	fmt.Printf("type=%v kind=%v\n", typ, typ.Kind())
}

func main() {
	describe(nil)
	describe(map[string]int{"go": 16})
}

一种可能的输出结果:

1
2
nil value
type=map[string]int kind=map

说明:reflect.TypeOf(nil) 会直接返回 nil,不能继续立刻调用 Kind()

第 5 题:遍历结构体字段、tag 和当前值

第 5 题 简单

编写一个 Go 程序,完成下面要求:

  • 定义结构体 Course
  • 字段如下:
1
2
3
4
5
type Course struct {
	Title   string `json:"title"`
	Price   int    `json:"price"`
	Teacher string `json:"teacher"`
}
  • 创建一个 Course
  • 使用反射遍历所有字段
  • 逐行输出字段名、json tag、字段 Kind 和字段当前值

输出结果应类似这样:

1
2
3
Title title string Go 反射
Price price int 99
Teacher teacher 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
26
27
28
29
package main

import (
	"fmt"
	"reflect"
)

type Course struct {
	Title   string `json:"title"`
	Price   int    `json:"price"`
	Teacher string `json:"teacher"`
}

func main() {
	course := Course{
		Title:   "Go 反射",
		Price:   99,
		Teacher: "阿斌",
	}

	typ := reflect.TypeOf(course)
	val := reflect.ValueOf(course)

	for i := 0; i < typ.NumField(); i++ {
		field := typ.Field(i)
		fieldValue := val.Field(i)
		fmt.Println(field.Name, field.Tag.Get("json"), fieldValue.Kind(), fieldValue.Interface())
	}
}

说明:这道题把结构体、tag 和反射放到了一起,和前面结构体章节里的 json tag 正好接上。

一般

第 6 题:实现一个可修改整数的反射函数

第 6 题 一般

编写函数 setScore(ptr any, score int) error,要求如下:

  • 传入的 ptr 必须是一个指针
  • 指针指向的值必须是 int,或者底层类型为 int 的自定义类型
  • 如果可以修改,就把它设置成新的分数
  • 如果不满足条件,返回错误

main 中测试下面这个场景:

1
2
3
4
type Score int

var score Score = 60
err := setScore(&score, 95)

如果成功,最后输出:

1
95
查看参考答案
 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 Score int

func setScore(ptr any, score int) error {
	value := reflect.ValueOf(ptr)
	if !value.IsValid() {
		return fmt.Errorf("nil value")
	}

	if value.Kind() != reflect.Ptr {
		return fmt.Errorf("ptr must be a pointer")
	}

	if value.IsNil() {
		return fmt.Errorf("ptr is nil")
	}

	target := value.Elem()
	if target.Kind() != reflect.Int {
		return fmt.Errorf("target kind must be int")
	}

	if !target.CanSet() {
		return fmt.Errorf("target cannot be set")
	}

	target.SetInt(int64(score))
	return nil
}

func main() {
	var score Score = 60

	err := setScore(&score, 95)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(score)
}

说明:Score 虽然是自定义类型,但它的 Kind 仍然是 int,所以这里可以用 SetInt 修改。

第 7 题:按字段名修改结构体字段

第 7 题 一般

编写函数 setFieldByName(dst any, fieldName string, newValue any) error,要求如下:

  • dst 必须是结构体指针
  • 使用 FieldByName 找到目标字段
  • 只处理 stringint 两种字段
  • 字段不存在、不可修改或类型不匹配时返回错误

main 中至少完成下面两次修改:

  • User.Name 改成 新名字
  • User.Age 改成 20
查看参考答案
 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package main

import (
	"fmt"
	"reflect"
)

type User struct {
	Name string
	Age  int
}

func setFieldByName(dst any, fieldName string, newValue any) error {
	value := reflect.ValueOf(dst)
	if value.Kind() != reflect.Ptr || value.IsNil() {
		return fmt.Errorf("dst must be a non-nil pointer")
	}

	target := value.Elem()
	if target.Kind() != reflect.Struct {
		return fmt.Errorf("dst must point to a struct")
	}

	field := target.FieldByName(fieldName)
	if !field.IsValid() {
		return fmt.Errorf("field %s not found", fieldName)
	}

	if !field.CanSet() {
		return fmt.Errorf("field %s cannot be set", fieldName)
	}

	switch field.Kind() {
	case reflect.String:
		text, ok := newValue.(string)
		if !ok {
			return fmt.Errorf("field %s needs string", fieldName)
		}
		field.SetString(text)
	case reflect.Int:
		number, ok := newValue.(int)
		if !ok {
			return fmt.Errorf("field %s needs int", fieldName)
		}
		field.SetInt(int64(number))
	default:
		return fmt.Errorf("unsupported kind: %v", field.Kind())
	}

	return nil
}

func main() {
	user := User{Name: "旧名字", Age: 18}

	if err := setFieldByName(&user, "Name", "新名字"); err != nil {
		fmt.Println(err)
		return
	}

	if err := setFieldByName(&user, "Age", 20); err != nil {
		fmt.Println(err)
		return
	}

	fmt.Printf("%+v\n", user)
}

一种可能的输出结果:

1
{Name:新名字 Age:20}

说明:这道题会自然复习结构体、字段名、any、类型断言和错误返回。

第 8 题:通过反射调用 Hello 方法

第 8 题 一般

编写函数 callMethod(value any, methodName string, arg string) (string, error),要求如下:

  • 使用 MethodByName 查找方法
  • 方法不存在时返回错误
  • 使用 Call 调用这个方法
  • 返回这个方法的第一个返回值

测试结构体如下:

1
2
3
4
5
6
7
type Student struct {
	Name string
}

func (student Student) Hello(prefix string) string {
	return prefix + "," + student.Name
}

main 中调用:

1
callMethod(Student{Name: "小李"}, "Hello", "你好")

期望结果:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import (
	"fmt"
	"reflect"
)

type Student struct {
	Name string
}

func (student Student) Hello(prefix string) string {
	return prefix + "," + student.Name
}

func callMethod(value any, methodName string, arg string) (string, error) {
	method := reflect.ValueOf(value).MethodByName(methodName)
	if !method.IsValid() {
		return "", fmt.Errorf("method %s not found", methodName)
	}

	results := method.Call([]reflect.Value{
		reflect.ValueOf(arg),
	})

	if len(results) == 0 {
		return "", fmt.Errorf("method %s returned no result", methodName)
	}

	return results[0].String(), nil
}

func main() {
	text, err := callMethod(Student{Name: "小李"}, "Hello", "你好")
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(text)
}

说明:Call 的参数和返回值都是 []reflect.Value,这和普通方法调用很不一样,也是反射代码最容易写错的地方之一。

第 9 题:修正把 json tag 当字段名查找的代码

第 9 题 一般

下面程序想修改 User.Name,但它现在找不到字段。
请改正代码,并说明原因。

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

import (
	"fmt"
	"reflect"
)

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

func main() {
	user := User{Name: "阿斌"}

	value := reflect.ValueOf(&user).Elem()
	field := value.FieldByName("name")
	field.SetString("小李")

	fmt.Println(user.Name)
}
查看参考答案

修正后的代码如下:

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

func main() {
	user := User{Name: "阿斌"}

	value := reflect.ValueOf(&user).Elem()
	field := value.FieldByName("Name")
	if field.IsValid() && field.CanSet() {
		field.SetString("小李")
	}

	fmt.Println(user.Name)
}

原因说明:

  • FieldByName 查找的是结构体真实字段名,不是 tag 名
  • 这里真实字段名是 Name,而 json:"name" 只是字段元信息
  • 如果想按 tag 查找,必须先遍历字段,再自己读取和比较 tag

第 10 题:实现支持结构体指针的 ToMap

第 10 题 一般

编写函数 ToMap(value any) (map[string]any, error),要求如下:

  • 支持传入结构体值或结构体指针
  • 如果字段有 db tag,就优先用 db tag 作为键
  • 如果没有 db tag,就使用字段名
  • 如果 db:"-",跳过这个字段
  • 如果传入的不是结构体或结构体指针,返回错误

测试结构体如下:

1
2
3
4
5
type Product struct {
	ID     int    `db:"id"`
	Name   string `db:"name"`
	Secret string `db:"-"`
}

main 中请测试:

1
ToMap(&Product{ID: 1, Name: "Go 手册", Secret: "hidden"})
查看参考答案
 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
48
49
50
51
52
53
54
55
56
57
58
59
60
package main

import (
	"fmt"
	"reflect"
)

type Product struct {
	ID     int    `db:"id"`
	Name   string `db:"name"`
	Secret string `db:"-"`
}

func ToMap(value any) (map[string]any, error) {
	val := reflect.ValueOf(value)
	if !val.IsValid() {
		return nil, fmt.Errorf("invalid value")
	}

	if val.Kind() == reflect.Ptr {
		if val.IsNil() {
			return nil, fmt.Errorf("nil pointer")
		}
		val = val.Elem()
	}

	if val.Kind() != reflect.Struct {
		return nil, fmt.Errorf("value must be a struct or *struct")
	}

	typ := val.Type()
	result := make(map[string]any)

	for i := 0; i < typ.NumField(); i++ {
		field := typ.Field(i)
		key := field.Tag.Get("db")
		if key == "-" {
			continue
		}
		if key == "" {
			key = field.Name
		}

		result[key] = val.Field(i).Interface()
	}

	return result, nil
}

func main() {
	product := &Product{ID: 1, Name: "Go 手册", Secret: "hidden"}

	result, err := ToMap(product)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(result)
}

一种可能的输出结果:

1
map[id:1 name:Go 手册]

说明:map 的输出顺序不固定,只要键值正确即可。这道题顺手复习了上一章的 tag,也把本章的简单映射示例往前推了一步。

进阶

下面四题会更接近真实工具函数里的反射用法。
做题过程中,你会自然用到前面学过的结构体、map、切片、函数、接口、错误处理和字符串转整数。

第 11 题:按 tag 把 map 绑定到结构体

第 11 题 进阶

编写函数 BindFromMap(dst any, data map[string]string) error,要求如下:

  • dst 必须是结构体指针
  • 根据字段上的 form tag 找到对应的输入值
  • 只处理 stringint 两种字段
  • int 字段需要把字符串转换成整数
  • 转换失败时返回错误

测试结构体如下:

1
2
3
4
type LessonForm struct {
	Title string `form:"title"`
	Count int    `form:"count"`
}

测试数据如下:

1
2
3
4
data := map[string]string{
	"title": "Go 反射",
	"count": "16",
}
查看参考答案
 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package main

import (
	"fmt"
	"reflect"
	"strconv"
)

type LessonForm struct {
	Title string `form:"title"`
	Count int    `form:"count"`
}

func BindFromMap(dst any, data map[string]string) error {
	value := reflect.ValueOf(dst)
	if value.Kind() != reflect.Ptr || value.IsNil() {
		return fmt.Errorf("dst must be a non-nil pointer")
	}

	target := value.Elem()
	if target.Kind() != reflect.Struct {
		return fmt.Errorf("dst must point to a struct")
	}

	typ := target.Type()

	for i := 0; i < typ.NumField(); i++ {
		fieldInfo := typ.Field(i)
		key := fieldInfo.Tag.Get("form")
		if key == "" {
			key = fieldInfo.Name
		}

		text, ok := data[key]
		if !ok {
			continue
		}

		field := target.Field(i)
		if !field.CanSet() {
			continue
		}

		switch field.Kind() {
		case reflect.String:
			field.SetString(text)
		case reflect.Int:
			number, err := strconv.Atoi(text)
			if err != nil {
				return fmt.Errorf("field %s: %w", fieldInfo.Name, err)
			}
			field.SetInt(int64(number))
		default:
			return fmt.Errorf("unsupported kind: %v", field.Kind())
		}
	}

	return nil
}

func main() {
	data := map[string]string{
		"title": "Go 反射",
		"count": "16",
	}

	var form LessonForm
	if err := BindFromMap(&form, data); err != nil {
		fmt.Println(err)
		return
	}

	fmt.Printf("%+v\n", form)
}

一种可能的输出结果:

1
{Title:Go 反射 Count:16}

说明:这已经很接近表单绑定、配置加载这类真实场景了。这里额外用到了 strconv.Atoi,因为反射能找到字段,但字符串到整数的转换仍然要我们自己做。

第 12 题:按方法名批量调用不同结构体

第 12 题 进阶

编写函数 callAll(list []any, methodName string, arg string) []string,要求如下:

  • 遍历切片中的每个值
  • 使用反射按方法名调用方法
  • 如果某个值没有这个方法,就在结果里追加 skip
  • 如果有这个方法,就把返回结果追加到结果切片里

测试类型如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
type Student struct {
	Name string
}

func (student Student) Speak(prefix string) string {
	return prefix + " 学生 " + student.Name
}

type Robot struct {
	ID string
}

func (robot Robot) Speak(prefix string) string {
	return prefix + " 机器人 " + robot.ID
}

测试切片如下:

1
2
3
4
5
list := []any{
	Student{Name: "小李"},
	Robot{ID: "R1"},
	18,
}

期望结果应类似这样:

1
[你好 学生 小李 你好 机器人 R1 skip]
查看参考答案
 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
48
49
50
51
52
53
54
55
56
57
package main

import (
	"fmt"
	"reflect"
)

type Student struct {
	Name string
}

func (student Student) Speak(prefix string) string {
	return prefix + " 学生 " + student.Name
}

type Robot struct {
	ID string
}

func (robot Robot) Speak(prefix string) string {
	return prefix + " 机器人 " + robot.ID
}

func callAll(list []any, methodName string, arg string) []string {
	results := make([]string, 0, len(list))

	for _, item := range list {
		method := reflect.ValueOf(item).MethodByName(methodName)
		if !method.IsValid() {
			results = append(results, "skip")
			continue
		}

		values := method.Call([]reflect.Value{
			reflect.ValueOf(arg),
		})

		if len(values) == 0 {
			results = append(results, "skip")
			continue
		}

		results = append(results, values[0].String())
	}

	return results
}

func main() {
	list := []any{
		Student{Name: "小李"},
		Robot{ID: "R1"},
		18,
	}

	fmt.Println(callAll(list, "Speak", "你好"))
}

一种可能的输出结果:

1
[你好 学生 小李 你好 机器人 R1 skip]

说明:这道题把反射方法调用、切片遍历和 any 放在了一起。能不能做这件事,不等于平时业务里就一定应该这样做,它更适合动态分发场景。

第 13 题:实现一个简化的插入字段构造器

第 13 题 进阶

请实现函数 buildInsertParts(value any) (fields []string, values []any, err error),要求如下:

  • 支持结构体值或结构体指针
  • 读取字段上的 db tag
  • 如果 db:"-",跳过这个字段
  • 按结构体字段原始顺序返回 fieldsvalues

测试结构体如下:

1
2
3
4
5
type User struct {
	Name     string `db:"name"`
	Age      int    `db:"age"`
	Password string `db:"-"`
}

调用:

1
buildInsertParts(User{Name: "阿斌", Age: 21, Password: "123456"})

期望结果应类似这样:

1
2
[name age]
[阿斌 21]
查看参考答案
 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package main

import (
	"fmt"
	"reflect"
)

type User struct {
	Name     string `db:"name"`
	Age      int    `db:"age"`
	Password string `db:"-"`
}

func buildInsertParts(value any) (fields []string, values []any, err error) {
	val := reflect.ValueOf(value)
	if !val.IsValid() {
		return nil, nil, fmt.Errorf("invalid value")
	}

	if val.Kind() == reflect.Ptr {
		if val.IsNil() {
			return nil, nil, fmt.Errorf("nil pointer")
		}
		val = val.Elem()
	}

	if val.Kind() != reflect.Struct {
		return nil, nil, fmt.Errorf("value must be a struct or *struct")
	}

	typ := val.Type()

	for i := 0; i < typ.NumField(); i++ {
		fieldInfo := typ.Field(i)
		key := fieldInfo.Tag.Get("db")
		if key == "-" {
			continue
		}
		if key == "" {
			key = fieldInfo.Name
		}

		fields = append(fields, key)
		values = append(values, val.Field(i).Interface())
	}

	return fields, values, nil
}

func main() {
	fields, values, err := buildInsertParts(User{
		Name:     "阿斌",
		Age:      21,
		Password: "123456",
	})
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(fields)
	fmt.Println(values)
}

输出结果:

1
2
[name age]
[阿斌 21]

说明:这道题比 ToMap 更接近 ORM 里“按字段顺序组织 SQL 参数”的思路,也会自然复习切片追加和结构体 tag。

第 14 题:先走接口校验,再做反射映射

第 14 题 进阶

请完成一个综合练习:

  • 定义接口 Validator
  • 约定它只有一个方法:Validate() error
  • 定义结构体 User
1
2
3
4
type User struct {
	Name string `db:"name"`
	Age  int    `db:"age"`
}
  • User 实现 Validate() error
  • Name == "" 时返回错误
  • Age <= 0 时返回错误
  • 编写函数 BuildPayload(value any) (map[string]any, error)
  • BuildPayload 需要先检查值是否实现了 Validator
  • 校验通过后,再使用反射读取结构体字段和 db tag
  • 返回一个 map[string]any
  • 同时支持结构体值和结构体指针

main 中至少测试两个场景:

  • 一个合法用户
  • 一个非法用户
查看参考答案
 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package main

import (
	"fmt"
	"reflect"
)

type Validator interface {
	Validate() error
}

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

func (user User) Validate() error {
	if user.Name == "" {
		return fmt.Errorf("name is empty")
	}
	if user.Age <= 0 {
		return fmt.Errorf("age must be greater than 0")
	}
	return nil
}

func BuildPayload(value any) (map[string]any, error) {
	val := reflect.ValueOf(value)
	if !val.IsValid() {
		return nil, fmt.Errorf("invalid value")
	}

	if val.Kind() == reflect.Ptr {
		if val.IsNil() {
			return nil, fmt.Errorf("nil pointer")
		}
	}

	if validator, ok := value.(Validator); ok {
		if err := validator.Validate(); err != nil {
			return nil, err
		}
	}

	if val.Kind() == reflect.Ptr {
		val = val.Elem()
	}

	if val.Kind() != reflect.Struct {
		return nil, fmt.Errorf("value must be a struct or *struct")
	}

	typ := val.Type()
	result := make(map[string]any)

	for i := 0; i < typ.NumField(); i++ {
		field := typ.Field(i)
		key := field.Tag.Get("db")
		if key == "" {
			key = field.Name
		}
		result[key] = val.Field(i).Interface()
	}

	return result, nil
}

func main() {
	okUser := User{Name: "阿斌", Age: 21}
	payload, err := BuildPayload(okUser)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(payload)

	badUser := User{Name: "", Age: 0}
	_, err = BuildPayload(badUser)
	if err != nil {
		fmt.Println(err)
	}
}

一种可能的输出结果:

1
2
map[age:21 name:阿斌]
name is empty

说明:这道题刻意把“接口校验”和“反射映射”放在一起,是为了练习本章最后一节提到的思路。能先用接口表达清楚的业务规则,就不要一上来全交给反射。

本文禁止转载
使用 Hugo 构建
主题 StackJimmy 设计 由 Hobin 魔改
最近构建时间:2026-04-17 19:07:48 CST
载入天数...载入时分秒...
发表了 1 篇文章 · 发表了 152 篇笔记 · 总计 18 万 0 千字