引入
写一个 Min 函数,int 写一遍,float64 又写一遍,string 还要写一遍。Go 1.18 引入泛型,用类型参数让一个函数或类型支持多种类型,不用再复制粘贴了。
正文
定义
定义
泛型(generics)通过类型参数(type parameter)让函数和类型支持多种类型,同时保持类型安全。类型参数用方括号
[]声明,后面跟类型约束(constraint)限制允许的类型范围。
语法
// 泛型函数
func Min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
// 泛型类型
type Set[T comparable] struct {
items map[T]bool
}例子
泛型函数
func Map[T any, U any](s []T, f func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = f(v)
}
return result
}
func main() {
nums := []int{1, 2, 3}
strs := Map(nums, func(n int) string {
return fmt.Sprintf("%d", n)
})
fmt.Println(strs) // [1 2 3]
}Map 函数接收两种类型参数 T 和 U,编译器根据调用时的参数自动推断类型。
类型约束
import "golang.org/x/exp/constraints"
func Sum[T constraints.Integer](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}constraints.Integer 约束 T 必须是整数类型,constraints.Ordered 约束 T 必须支持比较运算符。
自定义约束
type Number interface {
~int | ~float64
}
func Abs[T Number](n T) T {
if n < 0 {
return -n
}
return n
}用接口定义类型集合,~int 表示底层类型是 int 的所有类型,包括自定义类型。
泛型结构体
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
last := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return last, true
}泛型结构体让数据结构一次定义,多次复用,不用为每种类型各写一份。
类型推断
func main() {
// 显式指定类型参数
fmt.Println(Min[int](3, 5))
// 自动推断(大多数情况可以省略)
fmt.Println(Min(3, 5))
fmt.Println(Min("apple", "banana"))
}Go 编译器能从函数参数自动推断类型参数,不用每次手动写。
常见写法
| 写法 | 说明 |
|---|---|
func F[T any](…) | 泛型函数,T 可以是任意类型 |
func F[T comparable](…) | T 必须可比较(支持 ==) |
func F[T constraints.Ordered](…) | T 必须支持排序比较 |
type S[T any] struct{} | 泛型结构体 |
~int | 底层类型是 int 的所有类型 |
type C interface{ ~int | ~string } | 自定义类型约束 |
F[int](args) | 显式指定类型参数 |
F(args) | 编译器自动推断类型参数 |
特点
- 类型参数用方括号
[T constraint]声明 any是interface{}的别名,comparable要求类型支持==~前缀表示底层类型匹配,包括自定义类型- 编译器能自动推断类型参数,多数时候不用显式写
- 泛型是编译时检查,不是运行时反射,性能不受影响
constraints包提供Ordered、Integer、Float等常用约束- 不要滥用泛型,只在该函数需要支持多种类型时才用
理解
泛型就是给函数和类型加了一个”类型占位符”。编译器在编译时把它替换成具体类型,类型安全和性能都不打折。配合 constraints 包和自定义类型集合,能写出干净复用的通用代码,不用再给每种类型抄一遍了。
引出
泛型在编译时处理类型,但有时候需要在运行时动态获取和操作类型信息。接下来看 Go 反射,学习 reflect 包怎么在运行时检查类型和值。