Go 语言泛型
类型参数、约束、`~` 底层类型、以及什么时候不该用泛型。
Key PointsGo 1.18 引入泛型:`[T any]` 声明类型参数,用接口描述约束。
第一个泛型函数
Go
// 没有泛型时要为每种类型写一遍
func SumInts(m []int) int {
var s int
for _, v := range m {
s += v
}
return s
}
// 有了泛型,一个就够
func Sum[T int | int64 | float64](m []T) T {
var s T
for _, v := range m {
s += v
}
return s
}
fmt.Println(Sum([]int{1, 2, 3}))
fmt.Println(Sum([]float64{1.5, 2.5}))Output
6
4约束
Go
import "golang.org/x/exp/constraints"
// 用标准库约束
func Max[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
// 自定义约束接口
type Number interface {
~int | ~int64 | ~float64
}
func Avg[T Number](m []T) float64 {
var s T
for _, v := range m {
s += v
}
return float64(s) / float64(len(m))
}
fmt.Println(Max(3, 7))
fmt.Println(Avg([]int{1, 2, 3, 4}))Output
7
2.5The ~ in ~int means "any type whose underlying type is int", including type MyInt int. Without ~ only int itself matches.
泛型类型
Go
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
}
v := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return v, true
}
s := &Stack[string]{}
s.Push("a")
s.Push("b")
v, _ := s.Pop()
fmt.Println(v)Output
bcomparable
Go
// 需要 == 或做 map 键时,约束必须是 comparable
func Contains[T comparable](s []T, target T) bool {
for _, v := range s {
if v == target {
return true
}
}
return false
}
fmt.Println(Contains([]string{"a", "b"}, "b"))
fmt.Println(Contains([]int{1, 2}, 3))
// 用泛型写 map 的键
func Keys[K comparable, V any](m map[K]V) []K {
out := make([]K, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}Output
true
falsecomparable excludes slices, maps and functions — they are not comparable. Constraining to any then using == fails to compile, the most common generics error.什么时候不该用泛型
| Situation | Use |
|---|---|
| 只有一种类型 | 普通函数 |
| 行为不同、只是签名像 | 接口 |
| 运行时才知道类型 | any + 断言 |
| 多个类型有相同的算法 | 泛型 |
| 容器类型(栈、队列、缓存) | 泛型 |
The Go team is conservative here: generics exist for containers and generic algorithms, not to erase all duplication. If a function is used once, or behaviour differs per type, generics only hurt readability.
标准库里的泛型
Go
import (
"cmp"
"maps"
"slices"
)
s := []int{3, 1, 4, 1, 5}
slices.Sort(s)
fmt.Println(s)
fmt.Println(slices.Max(s), slices.Min(s))
fmt.Println(slices.Contains(s, 4))
m := map[string]int{"b": 2, "a": 1}
fmt.Println(slices.Sorted(maps.Keys(m)))
fmt.Println(cmp.Compare(1, 2))Output
[1 1 3 4 5]
5 1
true
[a b]
-1Since Go 1.21 the slices, maps and cmp packages cover the generic operations you used to hand-write. Check them before writing your own.
第一个泛型函数
Go
// 没有泛型时要为每种类型写一遍
func SumInts(m []int) int {
var s int
for _, v := range m {
s += v
}
return s
}
// 有了泛型,一个就够
func Sum[T int | int64 | float64](m []T) T {
var s T
for _, v := range m {
s += v
}
return s
}
fmt.Println(Sum([]int{1, 2, 3}))
fmt.Println(Sum([]float64{1.5, 2.5}))Output
6
4约束
Go
import "golang.org/x/exp/constraints"
// 用标准库约束
func Max[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
// 自定义约束接口
type Number interface {
~int | ~int64 | ~float64
}
func Avg[T Number](m []T) float64 {
var s T
for _, v := range m {
s += v
}
return float64(s) / float64(len(m))
}
fmt.Println(Max(3, 7))
fmt.Println(Avg([]int{1, 2, 3, 4}))Output
7
2.5~int 里的 ~ 表示「底层类型是 int 的所有类型」,包括 type MyInt int 这样的自定义类型。不加 ~ 只匹配 int 本身。
泛型类型
Go
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
}
v := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return v, true
}
s := &Stack[string]{}
s.Push("a")
s.Push("b")
v, _ := s.Pop()
fmt.Println(v)Output
bcomparable
Go
// 需要 == 或做 map 键时,约束必须是 comparable
func Contains[T comparable](s []T, target T) bool {
for _, v := range s {
if v == target {
return true
}
}
return false
}
fmt.Println(Contains([]string{"a", "b"}, "b"))
fmt.Println(Contains([]int{1, 2}, 3))
// 用泛型写 map 的键
func Keys[K comparable, V any](m map[K]V) []K {
out := make([]K, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}Output
true
falsecomparable 不包含切片、map、函数——它们不能比较。约束写成 any 后用 == 会编译报错,这是最常见的泛型编译错误。什么时候不该用泛型
| 场景 | 该用什么 |
|---|---|
| 只有一种类型 | 普通函数 |
| 行为不同、只是签名像 | 接口 |
| 运行时才知道类型 | any + 断言 |
| 多个类型有相同的算法 | 泛型 |
| 容器类型(栈、队列、缓存) | 泛型 |
Go 团队的立场很保守:泛型是为了写容器和通用算法,不是为了消除所有重复。如果一个函数只用一次,或者不同类型的行为不一样,泛型只会让代码更难读。
标准库里的泛型
Go
import (
"cmp"
"maps"
"slices"
)
s := []int{3, 1, 4, 1, 5}
slices.Sort(s)
fmt.Println(s)
fmt.Println(slices.Max(s), slices.Min(s))
fmt.Println(slices.Contains(s, 4))
m := map[string]int{"b": 2, "a": 1}
fmt.Println(slices.Sorted(maps.Keys(m)))
fmt.Println(cmp.Compare(1, 2))Output
[1 1 3 4 5]
5 1
true
[a b]
-1Go 1.21 起 slices、maps、cmp 三个包把过去要手写的通用操作都收进去了。写业务代码前先查一眼,往往已经有了。