在 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]语法定义类型参数。any是interface{}的别名,comparable用于可比较类型。泛型极大地简化了工具类代码的编写。
练习题
编写一个泛型函数
Contains[T comparable](s []T, e T) bool,判断切片 s 中是否包含元素 e。尝试编写一个泛型结构体
Map[K comparable, V any],并封装Put和Get方法。