需要预先了解:


1. 泛型

Go 1.18 引入了泛型(generics):函数和类型可以带类型参数,在调用或使用时才确定具体类型。它解决的是"同一套逻辑要写很多遍,只有类型不同"的问题——比复制粘贴好维护,比 any + 类型断言更安全,也比接口更直接(不需要为每种类型实现方法)。


1.1 从重复代码出发

先看一个没有泛型的场景:为 int 和 float64 各写一遍求最小值的函数。

func MinInt(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func MinFloat(a, b float64) float64 {
	if a < b {
		return a
	}
	return b
}

两个函数除了类型以外完全相同。用泛型可以合成一个:

func Min[T Ordered](a, b T) T {
	if a < b {
		return a
	}
	return b
}

[T Ordered] 是类型参数列表:T 是类型参数名,Ordered 是约束(constraint),规定 T 必须是支持 < 的类型。


1.2 约束

约束决定了类型参数能做什么操作。最宽松的约束是 any:

func Len[T any](s []T) int {
	return len(s)
}

any 只允许做与具体类型无关的操作(len、赋值、传参)。想比较相等就要用 comparable:

func Contains[T comparable](s []T, target T) bool {
	for _, v := range s {
		if v == target {
			return true
		}
	}
	return false
}

想比较大小,需要自己定义约束:

type Ordered interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 |
		~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
		~float32 | ~float64 | ~string
}

类型集与 ~

约束接口里用 | 列出的类型集合,叫类型集(type set):只有落在集合里的类型才能作为 T。

~int 表示"底层类型是 int 的所有类型",包括自定义类型:

type MyInt int

fmt.Println(Min[MyInt](3, 1)) // 1

如果只写 int(不带 ~),那么 MyInt 就不满足约束。需要同时支持内置类型和它们的自定义别名时,~ 几乎总是需要的。

标准库的 cmp.Ordered(Go 1.21+)就是上面那个约束的现成版本:

import "cmp"

func Min[T cmp.Ordered](a, b T) T { /* ... */ }

cmp 包还提供 cmp.Compare(a, b) 和 cmp.Less(a, b) 两个泛型函数。

带方法的约束

约束接口也可以写方法,此时类型必须实现这些方法:

type Stringer interface {
	String() string
	~string | ~int // 方法 + 类型集可以共存
}

实际写代码时,约束尽量小而精确:能只要求 comparable 就不要要求 Ordered,这样可用的类型更多。


1.3 泛型类型

类型也可以带类型参数:

type Stack[T any] struct {
	items []T
}

func NewStack[T any]() *Stack[T] {
	return &Stack[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 := NewStack[int]()
s.Push(1)
s.Push(2)
for v, ok := s.Pop(); ok; v, ok = s.Pop() {
	fmt.Println("弹出:", v)
}
弹出: 2
弹出: 1

注意 Pop 里用 var zero T 得到零值——类型参数不能用字面量表示零值,T(0)、T{} 都不行。

泛型类型的方法

方法声明里用的是接收者声明的类型参数,写法是 (s *Stack[T]),不需要再写一遍 [T any]。

Go 1.27 之前,方法不能再声明自己独立的类型参数;Go 1.27 起支持泛型方法(需要在 go.mod 里把 go 版本设为 1.27 及以上):

func (c *Container[T]) Map[U any](f func(T) U) []U {
	out := make([]U, 0, len(c.items))
	for _, v := range c.items {
		out = append(out, f(v))
	}
	return out
}

在低版本里编译会报:

generic method requires go1.27 or later (-lang was set to go1.24; check go.mod)

1.4 类型参数的限制

1. 不能对类型参数类型的值做类型断言。

func bad[T any](v T) int {
	return v.(int)
}
invalid operation: cannot use type assertion on type parameter value v (variable of type T constrained by any)

类型断言要求左边是接口值,而 T 不是接口值。先转成 any 就可以:

func good[T any](v T) (int, bool) {
	n, ok := any(v).(int)
	return n, ok
}

反过来,把类型参数当作断言的目标是允许的:

func tryAssert[T any](v any) (T, bool) {
	r, ok := v.(T)
	return r, ok
}

2. 不能用复合字面量创建类型参数的值。

func makeZero[T any]() T {
	return T{}
}
invalid composite literal type T (no common underlying type)

需要零值时用 var zero T,需要指针时用 new(T)。


1.5 泛型与接口的选型

维度 泛型 接口
多态时机 编译期(实例化) 运行时(动态派发)
性能 生成特化代码,无接口开销 有一次方法表查找,值可能装箱
约束表达 类型集 + 方法 方法集合
适用场景 容器、算法(同一套逻辑用于多种类型) 行为抽象(调用方只关心"能做什么")

选择建议:

  • 容器和算法用泛型:Stack[T]、Set[T]、Filter、Map;
  • 行为抽象用接口:一个函数需要"能写日志的东西",定义 Logger 接口即可;
  • 两者可以组合:func Process[T any](items []T, log Logger)。

注:不要为了"少写几个函数"而滥用泛型。如果只有两三个具体类型,直接写清楚类型反而更易读;泛型适合"逻辑确实与类型无关"的情况。


1.6 标准库泛型工具

Go 1.21 把 slices、maps、cmp 三个泛型包加入了标准库。之前它们以 golang.org/x/exp/slices 的形式存在。

s := []int{3, 1, 4, 1, 5}
slices.Sort(s)                    // [1 1 3 4 5]
slices.Contains(s, 4)             // true
slices.Index(s, 5)                // 4
slices.Max(s)                     // 5
slices.Min(s)                     // 1
slices.Compact([]int{1, 1, 2, 3, 3}) // [1 2 3]

Insert、Delete 会返回新切片,Clone 会复制一份底层数组:

s3 := []int{1, 2, 3}
s3 = slices.Insert(s3, 1, 99) // [1 99 2 3]
s3 = slices.Delete(s3, 0, 1)  // [99 2 3]

s4 := slices.Clone(s3)
s4[0] = 100
fmt.Println(s3, s4) // [99 2 3] [100 2 3]

maps 包提供 maps.Keys、maps.Values、maps.Clone 等操作。Go 1.23 起 maps.Keys 返回迭代器,配合 slices.Collect 使用:

keys := slices.Collect(maps.Keys(m))

1.7 实战:泛型集合

type Set[T comparable] map[T]struct{}

func NewSet[T comparable]() Set[T] {
	return make(Set[T])
}

func (s Set[T]) Add(v T)       { s[v] = struct{}{} }
func (s Set[T]) Has(v T) bool  { _, ok := s[v]; return ok }
func (s Set[T]) Remove(v T)    { delete(s, v) }

struct{} 是零大小的类型,用它作 value 可以避免额外内存:

set := NewSet[string]()
set.Add("a")
set.Add("b")
set.Add("a")
fmt.Println(set.Has("a"), set.Has("c"), len(set)) // true false 2

函数式工具同样可以泛型化:

func Map[T, U any](s []T, f func(T) U) []U {
	out := make([]U, 0, len(s))
	for _, v := range s {
		out = append(out, f(v))
	}
	return out
}

func Filter[T any](s []T, pred func(T) bool) []T {
	out := make([]T, 0, len(s))
	for _, v := range s {
		if pred(v) {
			out = append(out, v)
		}
	}
	return out
}
Map([]int{1, 2, 3}, func(n int) string { return fmt.Sprint(n * 2) }) // [2 4 6]
Filter([]int{1, 2, 3, 4}, func(n int) bool { return n%2 == 0 })      // [2 4]

泛型版函数式工具在 Go 里并不总是更好:链式调用会生成中间切片,闭包调用也无法内联。性能敏感的场景直接写 for 循环往往更快,见 性能分析与调优。


1.8 练习

  1. 写一个泛型 Reduce 函数,把 []T 归约为一个 U,并用它实现"求和"和"拼接字符串"两种调用。
  2. 定义约束 Number,实现泛型 Average 函数返回 float64,注意 T 与 float64 之间的转换需要用 float64(v)。
  3. 用 slices 和 maps 包统计一段文本中每个单词的出现次数,并输出出现次数最多的三个单词。