没有泛型的日子
假设要写一个求和函数,int 一份、float64 一份:
func SumInts(nums []int) int { var total int for _, n := range nums { total += n } return total}
func SumFloats(nums []float64) float64 { var total float64 for _, n := range nums { total += n } return total}两个函数除了类型外都一样,想避开重复通常只能用 any:
// 用 any(也就是 interface{})接,代价是每次都要类型断言,而且错误要等到运行期才会爆func SumAny(nums []any) any { var total int for _, n := range nums { total += n.(int) // 传进 float64 就 panic } return total}第一个泛型函数
泛型就像类型的参数,写在函数名后面的方括号里:
func Sum[T int | float64](nums []T) T { var total T for _, n := range nums { total += n } return total}[T int | float64]:声明一个类型参数T,后面那串是约束(Constraint),意思是T只能是int或float64。nums []T、返回值T:函数签名里就可以把T当成普通类型使用。var total T:T的零值。int是0、float64是0.0。
调用时通常什么都不用多写,编译器会从参数推断出 T:
Sum([]int{1, 2, 3}) // T 推断为 int,得到 6Sum([]float64{1.5, 2.5}) // T 推断为 float64,得到 4Sum[int]([]int{1, 2, 3}) // 也可以手动指定
Sum([]string{"a", "b"}) // 编译错误:string 不在约束里错误发生在编译期,不是运行期。这是泛型和 any 搭配类型断言最大的区别。
约束怎么写
约束本质上是一个 interface,只是它描述的不只是方法,还可以是「哪些类型可以代入」。
any:什么类型都行
any 作为约束时表示不限类型,但也因此不能对值做任何假设,只能搬移、比较指针、放进容器。经典例子是 Map:
func Map[T, U any](s []T, f func(T) U) []U { result := make([]U, 0, len(s)) for _, v := range s { result = append(result, f(v)) } return result}
names := Map([]int{1, 2, 3}, func(n int) string { return fmt.Sprintf("no.%d", n)})// [no.1 no.2 no.3]一个函数可以有多个类型参数,这里 T 是输入元素类型、U 是输出元素类型,两者由 f 的签名串起来。
comparable:可以用 == 比较
要在函数里写 == 或 !=,约束就得是 comparable:
func Contains[T comparable](s []T, target T) bool { for _, v := range s { if v == target { return true } } return false}
Contains([]string{"a", "b"}, "b") // truecomparable 涵盖数字、字符串、布尔值、指针、channel,以及字段全都可比较的 struct 和 array;slice、map、func 不在其中,因为它们本来就不能用 == 比较。
cmp.Ordered:可以用 < > 比较
comparable 只保证 ==,要比较大小得用标准库 cmp 提供的 cmp.Ordered:
import "cmp"
func Max[T cmp.Ordered](s []T) (T, bool) { var zero T if len(s) == 0 { return zero, false } m := s[0] for _, v := range s[1:] { if v > m { m = v } } return m, true}
Max([]int{3, 1, 4}) // 4, trueMax([]string{"b", "a", "c"}) // "c", true自定义约束与 ~ 符号
约束多了就把它抽成具名 interface:
type Number interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~float32 | ~float64}
func Sum[T Number](nums []T) T { var total T for _, n := range nums { total += n } return total}~int 的波浪号读作「底层类型是 int 的所有类型」。区别在这里:
type Celsius float64
Sum([]Celsius{36.5, 37.2}) // 约束写 ~float64 才能过,写 float64 会被挡下没有 ~ 的话,只有 float64 本身算数,Celsius 这种自定义类型会被排除。实践中写约束默认加 ~ 就对了。
约束里也可以放方法,这时它就和普通 interface 没什么两样:
type Stringer interface { String() string}
func JoinAll[T Stringer](items []T) string { parts := make([]string, 0, len(items)) for _, item := range items { parts = append(parts, item.String()) } return strings.Join(parts, ", ")}泛型类型
除了函数,struct 也可以有类型参数,例如一个类型安全的 Stack:
type Stack[T any] struct { items []T}
func NewStack[T any]() *Stack[T] { return &Stack[T]{}}
func (s *Stack[T]) Push(item T) { s.items = append(s.items, item)}
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 (s *Stack[T]) Len() int { return len(s.items)}
// Go 1.27 起,方法也可以声明自己的类型参数。func (s *Stack[T]) MapTo[U any](f func(T) U) *Stack[U] { result := NewStack[U]() for _, item := range s.items { result.Push(f(item)) } return result}用起来:
s := NewStack[string]()s.Push("a")s.Push("b")
v, ok := s.Pop() // "b", trues.Push(42) // 编译错误:类型已经被锁定为 string
lengths := s.MapTo(func(v string) int { return len(v) })lengths.Push(42) // OK,lengths 是 *Stack[int]- 创建类型实例时不能省略类型参数,
&Stack{}不合法,要写&Stack[string]{}(所以才常搭配一个NewStack[T]()构造函数让推断生效)。 - 方法的 receiver 要带上
[T]。Go 1.27 起,方法本身也能声明新的类型参数,如上例的MapTo[U any];在旧版 Go 中,这种需求只能写成独立函数。 - interface 的方法仍然不能声明类型参数,而且 generic method 不能用来实现 interface method。
标准库已经帮你写好了
上面示范的 Contains、Max 其实不用自己实现,slices 与 maps 这两个泛型包已经涵盖大部分日常需求:
import ( "maps" "slices")
nums := []int{3, 1, 4, 1, 5}
slices.Contains(nums, 4) // trueslices.Index(nums, 4) // 2slices.Max(nums) // 5slices.Sort(nums) // 原地排序为 [1 1 3 4 5]slices.Reverse(nums)
people := []Person{{Name: "b"}, {Name: "a"}}slices.SortFunc(people, func(x, y Person) int { return cmp.Compare(x.Name, y.Name)})
m := map[string]int{"a": 1, "b": 2}keys := slices.Collect(maps.Keys(m)) // maps.Keys 返回的是 iteratorslices.Sort(keys) // map 遍历顺序随机,要排序自己来什么时候不要用泛型
泛型解决的是「同一段逻辑要套在多种类型上」,不是「制造多余的抽象」:
- 只有一种类型会用到就别写泛型,等真的出现第二种再改,Go 的惯例一向是先重复再抽象。
- 只需要调用方法就用普通 interface。像
func Print(s fmt.Stringer)这种,写成func Print[T fmt.Stringer](s T)没有得到任何好处,只是多打字。区别在于泛型保留了具体类型(可以返回T、放进[]T),普通 interface 则会抹掉。 - 逻辑因类型而异就别硬塞。如果函数里开始出现对类型分支的判断,那代表这其实是两个函数。
总结
| 概念 | 写法 | 用途 |
|---|---|---|
| 类型参数 | func F[T any](...) | 把类型变成参数 |
| 类型推断 | F(v) 而非 F[int](v) | 大多数情况下无需手动指定 |
any | [T any] | 不限类型,但不能对值做假设 |
comparable | [T comparable] | 允许 ==、!= |
cmp.Ordered | [T cmp.Ordered] | 允许 <、> |
~ | ~int | ~float64 | 涵盖底层类型相同的自定义类型 |
| 泛型类型 | type Stack[T any] struct | 类型安全的容器 |
类型参数写在方括号里、约束决定你能对值做什么
延伸阅读
- Tutorial: Getting started with generics - go.dev
- An Introduction To Generics - The Go Blog
- When To Use Generics - The Go Blog
- slices package - pkg.go.dev