在 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] 语法定义类型参数。
  • anyinterface{} 的别名,comparable 用于可比较类型。
  • 泛型极大地简化了工具类代码的编写。

练习题

  1. 编写一个泛型函数 Contains[T comparable](s []T, e T) bool,判断切片 s 中是否包含元素 e。
  2. 尝试编写一个泛型结构体 Map[K comparable, V any],并封装 PutGet 方法。

相关阅读