Go 1.18 引入的泛型(Generics)是 Go 历史上最具革命性的特性。它让代码复用和类型安全达到了新高度。

基本语法

泛型函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// T 是类型参数,any 是约束(表示任意类型)
func Map[T, 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
}

// 使用
ints := []int{1, 2, 3}
strings := Map(ints, func(i int) string {
return fmt.Sprintf("#%d", i)
})
// strings: []string{"#1", "#2", "#3"}

泛型类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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) {
if len(s.items) == 0 {
var zero T
return zero, false
}
v := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return v, true
}

// 使用
intStack := &Stack[int]{}
intStack.Push(42)
v, ok := intStack.Pop() // v 是 int 类型

stringStack := &Stack[string]{}
stringStack.Push("hello")

类型参数推断

Go 1.18+ 支持从函数参数推断类型参数:

1
2
3
4
5
6
7
8
// 可以省略类型参数
strings := Map(ints, func(i int) string {
return fmt.Sprintf("#%d", i)
})
// 等价于
strings := Map[int, string](ints, func(i int) string {
return fmt.Sprintf("#%d", i)
})

类型约束(Constraints)

约束定义了类型参数必须满足的要求。

内置约束

1
2
3
4
5
6
7
8
9
10
11
func Identity[T any](v T) T { return v }

// comparable: 可比较的类型(可用 == 和 !=)
func Index[T comparable](s []T, v T) int {
for i, item := range s {
if item == v {
return i
}
}
return -1
}

自定义约束

约束本质上是接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义约束
type Number interface {
~int | ~int64 | ~float64 | ~float32
}

// ~ 表示底层类型匹配即可(包括基于这些类型的别名)
type MyInt int

func Add[T Number](a, b T) T {
return a + b
}

// 使用
var a MyInt = 10
var b MyInt = 20
c := Add(a, b) // c 是 MyInt 类型

约束组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Stringer interface {
String() string
}

// 同时要求 comparable 和 String() 方法
type Printable interface {
comparable
Stringer
}

func PrintEqual[T Printable](a, b T) {
if a == b {
fmt.Println(a.String())
}
}

cmp 包

标准库 cmp 包(Go 1.21+)提供了有序类型的约束:

1
2
3
4
5
6
7
8
9
import "cmp"

// cmp.Ordered: 支持 < > <= >= 比较的类型
func Min[T cmp.Ordered](a, b T) T {
if a < b {
return a
}
return b
}

注意:标准库 cmp 包除 Ordered 约束外,还提供了 CompareLess 两个泛型函数。如需 SignedUnsignedIntegerFloat 等更细粒度的约束,可使用 golang.org/x/exp/constraints(实验性包)。

实用示例

1. 泛型链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Node[T any] struct {
Value T
Next *Node[T]
}

type LinkedList[T any] struct {
Head *Node[T]
}

func (l *LinkedList[T]) Prepend(v T) {
l.Head = &Node[T]{Value: v, Next: l.Head}
}

func (l *LinkedList[T]) Find(f func(T) bool) *Node[T] {
for n := l.Head; n != nil; n = n.Next {
if f(n.Value) {
return n
}
}
return nil
}

2. 泛型 Map 工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 过滤
func Filter[T any](s []T, f func(T) bool) []T {
result := make([]T, 0)
for _, v := range s {
if f(v) {
result = append(result, v)
}
}
return result
}

// 归约
func Reduce[T any, U any](s []T, init U, f func(U, T) U) U {
result := init
for _, v := range s {
result = f(result, v)
}
return result
}

// 使用
nums := []int{1, 2, 3, 4, 5, 6}
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
sum := Reduce(evens, 0, func(a, b int) int { return a + b })

3. 泛型单例

1
2
3
4
5
6
7
8
9
10
11
type Singleton[T any] struct {
instance *T
once sync.Once
}

func (s *Singleton[T]) Get(f func() *T) *T {
s.once.Do(func() {
s.instance = f()
})
return s.instance
}

4. 泛型 Repository 模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Repository[T any] interface {
Get(id string) (*T, error)
List() ([]*T, error)
Create(t *T) error
Update(t *T) error
Delete(id string) error
}

type User struct {
ID string
Name string
}

// 可以直接使用泛型实现
type InMemoryRepo[T any] struct {
data map[string]*T
}

func NewInMemoryRepo[T any]() *InMemoryRepo[T] {
return &InMemoryRepo[T]{data: make(map[string]*T)}
}

高级技巧

1. 类型约束的巧妙用法

1
2
3
4
5
6
7
8
9
10
// 限制只能是特定结构体的指针
type Entity interface {
*User | *Product | *Order
GetID() string
}

func Save[T Entity](e T) error {
id := e.GetID()
// ...
}

2. 泛型方法

Go 不支持泛型方法,只能在泛型类型上定义方法:

1
2
3
4
5
6
7
8
9
10
11
// 错误:方法不能有类型参数
type Box struct{}
func (b Box) Map[T any](f func(T) T) T { } // 编译错误

// 正确:类型必须是泛型的
type Box[T any] struct {
value T
}
func (b Box[T]) Map(f func(T) T) Box[T] {
return Box[T]{value: f(b.value)}
}

3. 泛型与接口

泛型类型可以实现接口:

1
2
3
4
5
6
7
8
9
10
11
12
type Container interface {
Len() int
}

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

func (b *Box[T]) Len() int { return len(b.items) }

// Box[T] 实现了 Container 接口
var _ Container = (*Box[int])(nil)

常见陷阱

1. 零值问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func First[T any](s []T) T {
if len(s) == 0 {
var zero T // 返回类型的零值
return zero
}
return s[0]
}

// 更好的做法是返回可选值
func SafeFirst[T any](s []T) (T, bool) {
if len(s) == 0 {
var zero T
return zero, false
}
return s[0], true
}

2. 约束过松

1
2
3
4
5
6
7
8
9
// 太松:任何类型都可以,但函数内部用了 + 操作
func Add[T any](a, b T) T {
return a + b // 编译错误!
}

// 正确:使用约束限制
func Add[T Number](a, b T) T {
return a + b
}

3. 约束过紧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 过紧:只接受 int
func Double(s []int) []int {
result := make([]int, len(s))
for i, v := range s {
result[i] = v * 2
}
return result
}

// 更好:使用泛型
func Double[T Number](s []T) []T {
result := make([]T, len(s))
for i, v := range s {
result[i] = v * 2
}
return result
}

4. 滥用泛型

不是所有地方都需要泛型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 不需要泛型:简单场景
func PrintInts(s []int) {
for _, v := range s {
fmt.Println(v)
}
}

// 过度设计
func PrintSlice[T any](s []T) {
for _, v := range s {
fmt.Println(v)
}
}

// 只有当你需要处理多种类型且保持类型安全时,才用泛型

性能考虑

1. GC Shape 特化(并非逐类型特化)

Go 泛型并非为每个具体类型生成一份独立代码,而是采用 GC Shape Stenciling(GC 形状特化)+ 字典 的实现策略:

  • 编译器按 GC shape(垃圾回收表示相同的类型归为一组)生成一份代码,而非每个类型一份。
  • 所有指针类型共享同一个 GC shape,只生成一份实例化代码。
  • 相同大小、对齐方式的类型(如 intfloat64)也可能共享代码。
  • 运行时通过 字典(dictionary) 传递类型信息(如方法分派、类型断言所需的数据)。
1
2
3
// 这两次调用:指针共享一份代码,int 与 string 各占一份 GC shape
ints := Map([]*int{...}, func(i *int) *int { ... })
strings := Map([]string{"a", "b"}, func(s string) string { ... })

因此在泛型函数中通过类型参数进行方法调用或类型断言时,存在一次字典查找的轻微开销;而纯粹的算术运算(如 +-)由于约束已知,往往能被内联优化掉,开销可忽略。

2. 避免在热路径用泛型做不必要的间接

对于性能极度敏感且类型固定的热路径,直接使用具体类型可以省去字典查找与泛型分派:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 泛型版本(可能有字典查找开销)
func Sum[T Number](s []T) T {
var sum T
for _, v := range s {
sum += v
}
return sum
}

// 具体类型版本(热路径更快)
func SumInt64(s []int64) int64 {
var sum int64
for _, v := range s {
sum += v
}
return sum
}

实测前先 benchmark:大多数场景下泛型开销可忽略,不要为了「可能的开销」而过早放弃复用性。

3. 内存布局

泛型类型的内存布局与具体类型完全相同,Stack[int] 与手写的 IntStack 在内存上没有区别,不存在额外开销。

最佳实践

1. 优先使用标准库

Go 1.21+ 标准库提供了很多泛型工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
import (
"slices"
"maps"
)

// 排序
slices.Sort([]int{3, 1, 2})

// 查找
idx := slices.Index([]string{"a", "b"}, "b")

// Map 克隆
m2 := maps.Clone(m1)

2. iter 包与 range-over-func(Go 1.23+)

Go 1.23 引入的范围遍历函数(range over func)与泛型结合,让自定义迭代器非常优雅:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import "iter"

// iter.Seq[V] 即 func(yield func(V) bool)
// iter.Seq2[K, V] 即 func(yield func(K, V) bool)

// 泛型生成器:返回一个惰性迭代器
func Map[T, U any](s []T, f func(T) U) iter.Seq[U] {
return func(yield func(U) bool) {
for _, v := range s {
if !yield(f(v)) {
return
}
}
}
}

// 直接用 range 遍历,无需先物化成切片
for s := range Map([]int{1, 2, 3}, func(i int) string {
return fmt.Sprintf("#%d", i)
}) {
fmt.Println(s)
}

相比返回切片,迭代器是惰性求值的,适合处理大数据集或无限序列。

3. 约束要恰到好处

1
2
3
4
5
6
7
8
// 太松
func Process[T any](v T) { }

// 太紧
func Process[T int | int64](v T) { }

// 恰到好处
func Process[T Number](v T) { }

4. 文档要清晰

1
2
3
4
5
// Sort 对切片进行排序
// T 必须是有序类型(支持 < 比较)
func Sort[T cmp.Ordered](s []T) {
// ...
}

5. 测试要覆盖多种类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func TestMin(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"ints", 1, 2, 1},
{"negative", -1, -2, -2},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Min(tt.a, tt.b); got != tt.want {
t.Errorf("Min(%v, %v) = %v, want %v", tt.a, tt.b, got, tt.want)
}
})
}
}

总结

Go 泛型的核心价值:

特性收益
类型参数代码复用,减少重复
类型约束编译期类型安全
类型推断使用简洁
GC Shape 单态化运行时近乎零开销

使用原则:

  1. 需要处理多种类型且保持类型安全 → 用泛型
  2. 类型固定或只需要一种类型 → 不用泛型
  3. 需要 interface{} + 类型断言 → 优先考虑泛型
  4. 性能敏感且类型固定 → 用具体类型