在 Go 1.18 之前,由于缺乏泛型,我们想编写一个通用的 Min 函数(既能比对 int 也能比对 float),通常需要写两遍代码,或者使用性能较差的 interface{} 和反射。

Go 1.18 终于引入了 泛型 (Generics),允许我们在函数和类型定义中使用 类型参数 (Type Parameters)。这极大地提高了代码的复用性和类型安全性。

1. 泛型函数

先看一个传统的非泛型版本:

func MinInt(a, b int) int {
    if a < b { return a }
    return b
}
// 如果要支持 float,还得再写一个 MinFloat...

使用泛型重写:

import "fmt"

// [T int | float64] 定义了类型参数 T
// T 被限制为 int 或 float64(类型约束)
func Min[T int | float64](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func main() {
    // 显式实例化
    fmt.Println(Min[int](10, 20))

    // 隐式类型推导(推荐):编译器自动根据参数推导出 T 是 float64
    fmt.Println(Min(3.14, 1.59))
}

2. 自定义泛型类型

除了函数,结构体也可以是泛型的。比如我们要实现一个通用的栈(Stack)。

// Stack 是一个泛型结构体,T 可以是任何类型 (any)
type Stack[T any] struct {
    elements []T
}

func (s *Stack[T]) Push(v T) {
    s.elements = append(s.elements, v)
}

func (s *Stack[T]) Pop() T {
    if len(s.elements) == 0 {
        var zero T
        return zero // 返回 T 类型的零值
    }
    v := s.elements[len(s.elements)-1]
    s.elements = s.elements[:len(s.elements)-1]
    return v
}

func main() {
    // 实例化一个 int 类型的栈
    sInt := Stack[int]{}
    sInt.Push(10)
    sInt.Push(20)
    fmt.Println(sInt.Pop()) // 20

    // 实例化一个 string 类型的栈
    sStr := Stack[string]{}
    sStr.Push("hello")
    fmt.Println(sStr.Pop()) // hello
}

3. 类型约束 (Constraints) 与 any

[T constraint] 中,约束决定了泛型 T 可以做什么操作。

  • any:Go 1.18 新增关键字,等价于 interface{}。表示没有任何限制,但这也意味着你不能对 T 进行算术运算(因为 struct 也是 any,但 struct 不能相加)。

  • comparable:内置约束,表示 T 支持 ==!= 操作(适用于 Map 的 Key)。

  • 自定义约束接口

// 定义一个接口,包含一组类型
type Number interface {
    int | int64 | float64
}

// 现在 T 可以进行算术运算了,因为它被限制为数字类型
func Add[T Number](a, b T) T {
    return a + b
}

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 方法。


相关阅读