章节

13.5 练习

本篇通过 15 道分级练习巩固泛型函数、约束、泛型结构体、泛型切片与泛型 map,并能把泛型迁移到通用工具函数和基础数据建模场景中。

练习

学完本章你应该掌握

  • 能判断什么场景适合用泛型函数、泛型结构体、泛型切片或泛型 map,而不是把所有类型都硬塞进 any
  • 能写出 func Name[T Constraint](...)type Result[T any] struct {}type List[T any] []Ttype Dict[K comparable, V any] map[K]V 这类基础泛型定义。
  • 能根据函数体里的真实操作选择合适约束,例如 anycomparable~int 和自定义 Ordered 约束。
  • 能处理泛型代码里的常见细节,例如零值返回、value, ok 模式、泛型方法接收者要带 [T]、泛型 map 写入前要先初始化。
  • 能把前面学过的结构体、自定义类型、函数、高阶函数、切片、map 和错误处理自然带进泛型工具代码里,写出更可复用的基础组件。
  • 能识别并修正泛型常见错误,例如约束过宽却做比较、约束过窄导致复用性差、忘记写 comparable、把自定义类型和底层类型的支持范围想错。

简单

第 1 题:编写一个支持两类数字的泛型加法函数

第 1 题 简单

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

  • 定义约束 Number,只允许 intfloat64
  • 编写泛型函数 Add[T Number](a, b T) T
  • main 中分别调用: Add(12, 30) Add(19.5, 0.5)
  • 输出两次计算结果
查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import "fmt"

type Number interface {
	int | float64
}

func Add[T Number](a, b T) T {
	return a + b
}

func main() {
	fmt.Println(Add(12, 30))
	fmt.Println(Add(19.5, 0.5))
}

输出结果:

1
2
42
20

说明:这道题的重点是先把“允许哪些类型”写进约束,再在函数体里安全地使用 +

第 2 题:编写一个双类型参数的标签函数

第 2 题 简单

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

  • 编写泛型函数 BuildTag[T int, K string | int](lessonNo T, tag K) string
  • 返回格式如下:
1
lesson=13, tag=generic
  • main 中分别调用: BuildTag(13, "generic") BuildTag(8, 101)
查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import "fmt"

func BuildTag[T int, K string | int](lessonNo T, tag K) string {
	return fmt.Sprintf("lesson=%v, tag=%v", lessonNo, tag)
}

func main() {
	fmt.Println(BuildTag(13, "generic"))
	fmt.Println(BuildTag(8, 101))
}

一种可能的输出结果:

1
2
lesson=13, tag=generic
lesson=8, tag=101

说明:这道题的重点是体会一个泛型函数里可以同时声明多个类型参数,而且每个类型参数都可以有自己的约束。

第 3 题:修正 any 约束下无法比较的最大值函数

第 3 题 简单

下面程序的目标是输出:

1
2
5
java

但它现在不能通过编译。请改正代码,让它正常运行。

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

import "fmt"

func Max[T any](a, b T) T {
	if a > b {
		return a
	}
	return b
}

func main() {
	fmt.Println(Max(3, 5))
	fmt.Println(Max("go", "java"))
}
查看参考答案

修正后的代码如下:

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

import "fmt"

type Ordered interface {
	~int | ~float64 | ~string
}

func Max[T Ordered](a, b T) T {
	if a > b {
		return a
	}
	return b
}

func main() {
	fmt.Println(Max(3, 5))
	fmt.Println(Max("go", "java"))
}

原因说明:

  • any 只表示“可以是任意类型”,并不代表这些类型一定支持 >
  • 如果函数体里要做比较,就应该把约束收窄到“支持比较”的类型集合。

第 4 题:定义一个泛型结果结构体

第 4 题 简单

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

  • 定义泛型结构体 Result[T any]
  • 字段包括: Code int Message string Data T
  • 给它绑定方法 GetData() T
  • main 中分别创建: Result[string] Result[[]int]
  • 输出两个实例中的 Data
查看参考答案
 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
package main

import "fmt"

type Result[T any] struct {
	Code    int
	Message string
	Data    T
}

func (r Result[T]) GetData() T {
	return r.Data
}

func main() {
	lessonResult := Result[string]{
		Code:    200,
		Message: "ok",
		Data:    "泛型函数",
	}

	idsResult := Result[[]int]{
		Code:    200,
		Message: "ok",
		Data:    []int{101, 102, 103},
	}

	fmt.Println(lessonResult.GetData())
	fmt.Println(idsResult.GetData())
}

输出结果:

1
2
泛型函数
[101 102 103]

说明:方法接收者要写成 Result[T],不能只写 Result

第 5 题:实现泛型切片的 Last 方法

第 5 题 简单

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

  • 定义泛型切片类型 List[T any]
  • 给它绑定方法 Last() (T, bool)
  • 如果切片为空,返回 T 的零值和 false
  • 如果切片不为空,返回最后一个元素和 true
  • main 中分别测试: List[string]{"Go", "泛型", "练习"} List[int]{}
查看参考答案
 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"

type List[T any] []T

func (list List[T]) Last() (T, bool) {
	var zero T
	if len(list) == 0 {
		return zero, false
	}
	return list[len(list)-1], true
}

func main() {
	words := List[string]{"Go", "泛型", "练习"}
	lastWord, ok1 := words.Last()
	fmt.Println(lastWord, ok1)

	empty := List[int]{}
	lastNum, ok2 := empty.Last()
	fmt.Println(lastNum, ok2)
}

输出结果:

1
2
练习 true
0 false

说明:var zero T 是泛型代码里很常见的写法,适合在“找不到值”时返回对应类型的零值。

第 6 题:编写一个泛型字典的读取方法

第 6 题 简单

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

  • 定义泛型 map 类型 Dict[K comparable, V any]
  • 给它绑定方法 Get(key K) (V, bool)
  • main 中准备下面这张表:
1
2
3
4
courseHours := Dict[string, int]{
	"函数": 8,
	"接口": 6,
}
  • 先读取 "接口"
  • 再读取 "切片"
  • 输出两次读取结果
查看参考答案
 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"

type Dict[K comparable, V any] map[K]V

func (dict Dict[K, V]) Get(key K) (V, bool) {
	value, ok := dict[key]
	return value, ok
}

func main() {
	courseHours := Dict[string, int]{
		"函数": 8,
		"接口": 6,
	}

	value1, ok1 := courseHours.Get("接口")
	value2, ok2 := courseHours.Get("切片")

	fmt.Println(value1, ok1)
	fmt.Println(value2, ok2)
}

输出结果:

1
2
6 true
0 false

说明:K 要写成 comparable,因为 map 的键必须是可比较类型。

一般

第 7 题:让自定义分数类型也能使用泛型求和

第 7 题 一般

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

  • 定义自定义类型 Score,底层类型是 int
  • 编写泛型函数 Sum[T ~int](nums []T) T
  • 让它既能统计 []int,也能统计 []Score
  • main 中准备:
1
scores := []Score{78, 92, 85}
  • 输出总分
查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

type Score int

func Sum[T ~int](nums []T) T {
	var total T
	for _, num := range nums {
		total += num
	}
	return total
}

func main() {
	scores := []Score{78, 92, 85}
	fmt.Println(Sum(scores))
	fmt.Println(Sum([]int{1, 2, 3}))
}

输出结果:

1
2
255
6

说明:~int 的意思是“底层类型是 int 的类型也可以传进来”,所以 Score 也满足这个约束。

第 8 题:用泛型把键值对切片转成 map

第 8 题 一般

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

  • 定义泛型结构体 Pair[K comparable, V any]
  • 字段包括: Key K Value V
  • 编写泛型函数 ToMap[K comparable, V any](pairs []Pair[K, V]) map[K]V
  • main 中准备下面这组数据:
1
2
3
4
5
pairs := []Pair[string, int]{
	{Key: "变量", Value: 1},
	{Key: "函数", Value: 6},
	{Key: "泛型", Value: 13},
}
  • 把它转成 map 后,按下面顺序输出:
1
2
3
变量 -> 1
函数 -> 6
泛型 -> 13
查看参考答案
 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
package main

import "fmt"

type Pair[K comparable, V any] struct {
	Key   K
	Value V
}

func ToMap[K comparable, V any](pairs []Pair[K, V]) map[K]V {
	result := make(map[K]V)
	for _, pair := range pairs {
		result[pair.Key] = pair.Value
	}
	return result
}

func main() {
	pairs := []Pair[string, int]{
		{Key: "变量", Value: 1},
		{Key: "函数", Value: 6},
		{Key: "泛型", Value: 13},
	}

	result := ToMap(pairs)
	order := []string{"变量", "函数", "泛型"}
	for _, key := range order {
		fmt.Printf("%s -> %d\n", key, result[key])
	}
}

说明:这道题把泛型结构体、多类型参数、切片和 map 组合起来了,很接近真实开发里“把数据转成查找表”的小工具函数。

第 9 题:实现一个泛型 IndexOf

第 9 题 一般

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

  • 编写泛型函数 IndexOf[T comparable](list []T, target T) int
  • 如果找到了,返回第一次出现的下标
  • 如果没找到,返回 -1
  • main 中至少测试下面两组数据:
1
2
[]string{"Go", "接口", "泛型"}, "泛型"
[]int{3, 5, 7}, 4
查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import "fmt"

func IndexOf[T comparable](list []T, target T) int {
	for index, item := range list {
		if item == target {
			return index
		}
	}
	return -1
}

func main() {
	fmt.Println(IndexOf([]string{"Go", "接口", "泛型"}, "泛型"))
	fmt.Println(IndexOf([]int{3, 5, 7}, 4))
}

输出结果:

1
2
2
-1

说明:如果函数体里要用 == 比较元素,就应该把类型参数约束成 comparable

第 10 题:实现一个带错误返回的泛型查找函数

第 10 题 一般

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

  • 编写泛型函数 FindOrError[K comparable, V any](dict map[K]V, key K) (V, error)
  • 如果 key 存在,返回对应值和 nil
  • 如果 key 不存在,返回 V 的零值和错误信息
  • main 中准备下面这张表:
1
2
3
4
lessons := map[int]string{
	101: "变量",
	102: "切片",
}
  • 先查询 102
  • 再查询 999
查看参考答案
 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
package main

import "fmt"

func FindOrError[K comparable, V any](dict map[K]V, key K) (V, error) {
	value, ok := dict[key]
	if ok {
		return value, nil
	}

	var zero V
	return zero, fmt.Errorf("key %v not found", key)
}

func main() {
	lessons := map[int]string{
		101: "变量",
		102: "切片",
	}

	value, err := FindOrError(lessons, 102)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Printf("lesson=%s\n", value)
	}

	value, err = FindOrError(lessons, 999)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Printf("lesson=%s\n", value)
	}
}

一种可能的输出结果:

1
2
lesson=切片
key 999 not found

说明:这道题会自然复习前面异常处理章节里的 error 返回,同时也练到了泛型里的“零值返回”写法。

第 11 题:实现一个泛型集合 Set

第 11 题 一般

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

  • 定义泛型 map 类型 Set[T comparable]
  • 编写函数 NewSet[T comparable]() Set[T]
  • Set[T] 绑定两个方法: Add(value T) Has(value T) bool
  • main 中创建一个 Set[string]
  • 依次加入 "Go""泛型""Go"
  • 最后输出: 集合长度 是否包含 "泛型" 是否包含 "接口"
查看参考答案
 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"

type Set[T comparable] map[T]struct{}

func NewSet[T comparable]() Set[T] {
	return make(Set[T])
}

func (set Set[T]) Add(value T) {
	set[value] = struct{}{}
}

func (set Set[T]) Has(value T) bool {
	_, ok := set[value]
	return ok
}

func main() {
	set := NewSet[string]()
	set.Add("Go")
	set.Add("泛型")
	set.Add("Go")

	fmt.Printf("len=%d\n", len(set))
	fmt.Println("泛型", set.Has("泛型"))
	fmt.Println("接口", set.Has("接口"))
}

输出结果:

1
2
3
len=2
泛型 true
接口 false

说明:这里的 value 使用 struct{},是 Go 里实现集合时很常见的写法,因为它不需要额外存储有效负载。

进阶

下面四题会把泛型和前面学过的结构体、自定义类型、切片、map、高阶函数、错误处理自然放到一起,更接近真实开发里写“通用工具组件”的思路。

第 12 题:实现一个泛型 GroupBy

第 12 题 进阶

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

  • 定义结构体 Student
  • 字段包括: Name string City string Score int
  • 编写泛型函数:
1
GroupBy[K comparable, T any](items []T, keyFn func(T) K) map[K][]T
  • 它的作用是:按照 keyFn 返回的键把元素分组
  • main 中准备下面这组数据:
1
2
3
4
5
6
students := []Student{
	{Name: "阿斌", City: "上海", Score: 92},
	{Name: "小李", City: "北京", Score: 85},
	{Name: "小王", City: "上海", Score: 76},
	{Name: "小周", City: "深圳", Score: 88},
}
  • 按城市分组
  • 最后按下面顺序输出每个城市的人数:
1
2
3
上海: 2
北京: 1
深圳: 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
package main

import "fmt"

type Student struct {
	Name  string
	City  string
	Score int
}

func GroupBy[K comparable, T any](items []T, keyFn func(T) K) map[K][]T {
	result := make(map[K][]T)
	for _, item := range items {
		key := keyFn(item)
		result[key] = append(result[key], item)
	}
	return result
}

func main() {
	students := []Student{
		{Name: "阿斌", City: "上海", Score: 92},
		{Name: "小李", City: "北京", Score: 85},
		{Name: "小王", City: "上海", Score: 76},
		{Name: "小周", City: "深圳", Score: 88},
	}

	groups := GroupBy(students, func(s Student) string {
		return s.City
	})

	order := []string{"上海", "北京", "深圳"}
	for _, city := range order {
		fmt.Printf("%s: %d\n", city, len(groups[city]))
	}
}

说明:这道题把高阶函数和泛型组合起来了。真正想练的是“把分组逻辑抽象出来,而不是为每种结构体都单独写一版”。

第 13 题:实现一个泛型 MaxBy

第 13 题 进阶

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

  • 定义约束 Ordered,允许: ~int ~float64 ~string
  • 定义自定义类型 Score,底层类型是 int
  • 定义结构体 Student
  • 字段包括: Name string Score Score
  • 编写泛型函数:
1
MaxBy[T any, S Ordered](items []T, scoreFn func(T) S) (T, bool)
  • 如果切片为空,返回 T 的零值和 false
  • 否则返回“得分最高的元素”和 true
  • main 中找到最高分学生,并输出姓名、分数和 ok
查看参考答案
 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
package main

import "fmt"

type Ordered interface {
	~int | ~float64 | ~string
}

type Score int

type Student struct {
	Name  string
	Score Score
}

func MaxBy[T any, S Ordered](items []T, scoreFn func(T) S) (T, bool) {
	var zero T
	if len(items) == 0 {
		return zero, false
	}

	best := items[0]
	bestScore := scoreFn(best)

	for _, item := range items[1:] {
		score := scoreFn(item)
		if score > bestScore {
			best = item
			bestScore = score
		}
	}

	return best, true
}

func main() {
	students := []Student{
		{Name: "阿斌", Score: 92},
		{Name: "小李", Score: 85},
		{Name: "小张", Score: 95},
	}

	top, ok := MaxBy(students, func(s Student) Score {
		return s.Score
	})

	fmt.Println(top.Name, top.Score, ok)
}

输出结果:

1
小张 95 true

说明:这里的 Score 虽然是自定义类型,但因为底层类型是 int,所以在 ~int 约束下仍然可以参与比较。

第 14 题:实现一个泛型分页结果转换器

第 14 题 进阶

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

  • 定义泛型结构体 Page[T any]
  • 字段包括: Items []T Total int
  • 编写泛型函数:
1
MapPage[T any, U any](page Page[T], mapper func(T) U) Page[U]
  • 它的作用是: 保留 TotalItems 中的每个元素转换成另一种类型
  • main 中定义结构体 Student
  • 准备下面这个分页结果:
1
2
3
4
5
6
7
Page[Student]{
	Total: 2,
	Items: []Student{
		{Name: "阿斌", Score: 92},
		{Name: "小李", Score: 85},
	},
}
  • 把它转换成 Page[string]
  • 每一项都变成:
1
阿斌-92
查看参考答案
 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
package main

import "fmt"

type Page[T any] struct {
	Items []T
	Total int
}

type Student struct {
	Name  string
	Score int
}

func MapPage[T any, U any](page Page[T], mapper func(T) U) Page[U] {
	result := make([]U, 0, len(page.Items))
	for _, item := range page.Items {
		result = append(result, mapper(item))
	}

	return Page[U]{
		Items: result,
		Total: page.Total,
	}
}

func main() {
	page := Page[Student]{
		Total: 2,
		Items: []Student{
			{Name: "阿斌", Score: 92},
			{Name: "小李", Score: 85},
		},
	}

	namePage := MapPage(page, func(s Student) string {
		return fmt.Sprintf("%s-%d", s.Name, s.Score)
	})

	fmt.Println(namePage.Total)
	fmt.Println(namePage.Items)
}

输出结果:

1
2
2
[阿斌-92 小李-85]

说明:这道题会让你体会到,泛型不只是“少写几份函数”,还可以让“数据结构本身”和“转换逻辑”一起复用。

第 15 题:合并两张泛型计数表

第 15 题 进阶

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

  • 定义自定义类型 LessonCount,底层类型是 int
  • 编写泛型函数:
1
MergeCounts[K comparable, N ~int](a, b map[K]N) map[K]N
  • 它的作用是: 把两张计数表按 key 合并 相同 key 的值相加 不同 key 保留原值
  • main 中准备下面两张表:
1
2
3
4
5
6
7
8
9
week1 := map[string]LessonCount{
	"泛型函数":   2,
	"泛型结构体": 1,
}

week2 := map[string]LessonCount{
	"泛型结构体": 2,
	"泛型map":   3,
}
  • 最后按下面顺序输出:
1
2
3
泛型函数: 2
泛型结构体: 3
泛型map: 3
查看参考答案
 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
package main

import "fmt"

type LessonCount int

func MergeCounts[K comparable, N ~int](a, b map[K]N) map[K]N {
	result := make(map[K]N)

	for key, value := range a {
		result[key] = value
	}

	for key, value := range b {
		result[key] += value
	}

	return result
}

func main() {
	week1 := map[string]LessonCount{
		"泛型函数":   2,
		"泛型结构体": 1,
	}

	week2 := map[string]LessonCount{
		"泛型结构体": 2,
		"泛型map":   3,
	}

	merged := MergeCounts(week1, week2)
	order := []string{"泛型函数", "泛型结构体", "泛型map"}
	for _, key := range order {
		fmt.Printf("%s: %d\n", key, merged[key])
	}
}

说明:这道题把泛型 map、~int、自定义类型和固定顺序输出组合到了一起,已经很接近“写一段可复用的小工具函数”的真实感觉了。

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