在 Go 1.18 之前,由于缺乏泛型,我们想编写一个通用的 Min 函数(既能比对 int 也能比对 float),通常需要写两遍代码,或者使用性能较差的 interface{} 和反射。
Go 1.18 终于引入了 泛型 (Generics),允许我们在函数和类型定义中使用 类型参数 (Type Parameters)。这极大地提高了代码的复用性和类型安全性。
1. 泛型函数
先看一个传统的非泛型版本:
1func MinInt(a, b int) int {
2 if a < b { return a }
3 return b
4}
5// 如果要支持 float,还得再写一个 MinFloat...
使用泛型重写:
1import "fmt"
2
3// [T int | float64] 定义了类型参数 T
4// T 被限制为 int 或 float64(类型约束)
5func Min[T int | float64](a, b T) T {
6 if a < b {
7 return a
8 }
9 return b
10}
11
12func main() {
13 // 显式实例化
14 fmt.Println(Min[int](10, 20))
15
16 // 隐式类型推导(推荐):编译器自动根据参数推导出 T 是 float64
17 fmt.Println(Min(3.14, 1.59))
18}
2. 自定义泛型类型
除了函数,结构体也可以是泛型的。比如我们要实现一个通用的栈(Stack)。
1// Stack 是一个泛型结构体,T 可以是任何类型 (any)
2type Stack[T any] struct {
3 elements []T
4}
5
6func (s *Stack[T]) Push(v T) {
7 s.elements = append(s.elements, v)
8}
9
10func (s *Stack[T]) Pop() T {
11 if len(s.elements) == 0 {
12 var zero T
13 return zero // 返回 T 类型的零值
14 }
15 v := s.elements[len(s.elements)-1]
16 s.elements = s.elements[:len(s.elements)-1]
17 return v
18}
19
20func main() {
21 // 实例化一个 int 类型的栈
22 sInt := Stack[int]{}
23 sInt.Push(10)
24 sInt.Push(20)
25 fmt.Println(sInt.Pop()) // 20
26
27 // 实例化一个 string 类型的栈
28 sStr := Stack[string]{}
29 sStr.Push("hello")
30 fmt.Println(sStr.Pop()) // hello
31}
3. 类型约束 (Constraints) 与 any
在 [T constraint] 中,约束决定了泛型 T 可以做什么操作。
any:Go 1.18 新增关键字,等价于interface{}。表示没有任何限制,但这也意味着你不能对T进行算术运算(因为 struct 也是 any,但 struct 不能相加)。comparable:内置约束,表示 T 支持==和!=操作(适用于 Map 的 Key)。- 自定义约束接口:
1// 定义一个接口,包含一组类型
2type Number interface {
3 int | int64 | float64
4}
5
6// 现在 T 可以进行算术运算了,因为它被限制为数字类型
7func Add[T Number](a, b T) T {
8 return a + b
9}
4. 什么时候使用泛型?
Go 官方建议:如果你发现自己在多次编写完全相同的逻辑,只是类型不同(比如 Slice 排序、Map 转换、Set 集合),那么请使用泛型。
不要为了用泛型而用泛型。如果接口(Interface)能解决问题,优先使用接口。泛型主要用于编写通用的工具库和数据结构。
小结
- 泛型允许编写与类型无关的代码。
- 使用
[T constraints]语法定义类型参数。 any是interface{}的别名,comparable用于可比较类型。- 泛型极大地简化了工具类代码的编写。
练习题
- 编写一个泛型函数
Contains[T comparable](s []T, e T) bool,判断切片 s 中是否包含元素 e。 - 尝试编写一个泛型结构体
Map[K comparable, V any],并封装Put和Get方法。