跳到主要内容

泛型

问题

Go 1.18 引入的泛型如何使用?类型约束是什么?有哪些实际应用场景?

答案

泛型基础语法

Go 1.18(2022 年 3 月)引入了类型参数(Type Parameters),又称泛型:

// 泛型函数:T 是类型参数,comparable 是类型约束
func Contains[T comparable](s []T, target T) bool {
for _, v := range s {
if v == target {
return true
}
}
return false
}

// 使用(类型可推断,不必显式写出)
Contains([]int{1, 2, 3}, 2) // true
Contains([]string{"a", "b"}, "c") // false
Contains[int]([]int{1, 2}, 3) // 显式指定类型参数

类型约束

类型约束定义了类型参数必须满足的条件。约束本质上是一个接口

// 内置约束
// any = interface{} (任意类型)
// comparable :支持 == 和 != 的类型

// 自定义约束
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
}

Sum([]int{1, 2, 3}) // 6
Sum([]float64{1.1, 2.2}) // 3.3

近似约束 ~

~ 表示底层类型匹配:

type MyInt int // 自定义类型,底层是 int

type Integer interface {
int // 只匹配 int,不匹配 MyInt
}

type AnyInteger interface {
~int // 匹配所有底层类型是 int 的类型(包括 MyInt)
}

func Double[T AnyInteger](x T) T {
return x * 2
}

var n MyInt = 5
Double(n) // 10(AnyInteger 用了 ~int,接受 MyInt)

泛型类型

// 泛型切片
type Stack[T any] struct {
items []T
}

func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}

// 使用
s := &Stack[int]{}
s.Push(1)
s.Push(2)
val, _ := s.Pop() // 2

标准库的泛型包

Go 1.21 引入了 slicesmaps 泛型包:

import (
"slices"
"maps"
)

// slices 包
nums := []int{3, 1, 4, 1, 5}
slices.Sort(nums) // [1, 1, 3, 4, 5]
slices.Contains(nums, 3) // true
idx := slices.Index(nums, 4) // 2
slices.Reverse(nums) // [5, 4, 3, 1, 1]
slices.Compact([]int{1, 1, 2, 2, 3}) // [1, 2, 3](去连续重复)

// maps 包
m := map[string]int{"a": 1, "b": 2}
keys := maps.Keys(m) // ["a", "b"](无序)
values := maps.Values(m) // [1, 2](无序)
maps.Equal(m1, m2) // 比较两个 map

实际应用场景

// 1. 通用数据结构
type Set[T comparable] map[T]struct{}

func NewSet[T comparable](items ...T) Set[T] {
s := make(Set[T])
for _, item := range items {
s[item] = struct{}{}
}
return s
}

func (s Set[T]) Contains(item T) bool {
_, ok := s[item]
return ok
}

// 2. 通用工具函数
func Map[T, U any](s []T, fn func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = fn(v)
}
return result
}

func Filter[T any](s []T, fn func(T) bool) []T {
var result []T
for _, v := range s {
if fn(v) {
result = append(result, v)
}
}
return result
}

// 使用
names := Map([]int{1, 2, 3}, strconv.Itoa) // ["1", "2", "3"]
evens := Filter([]int{1, 2, 3, 4}, func(n int) bool { return n%2 == 0 }) // [2, 4]

// 3. 类型安全的结果包装
type Result[T any] struct {
Value T
Err error
}

func NewResult[T any](val T, err error) Result[T] {
return Result[T]{Value: val, Err: err}
}

泛型的限制

// ❌ 不支持方法级类型参数(只能在类型或函数级别)
type Foo struct{}
// func (f Foo) Bar[T any](x T) {} // 编译错误

// ❌ 不支持特化(specialization)
// 不能为特定类型参数提供不同实现

// ❌ 类型约束不能用于运算符重载
// 不能定义 "支持 < 运算符" 的约束(除了 cmp.Ordered)

// ✅ 使用 cmp.Ordered 约束支持比较
import "cmp"

func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}

常见面试问题

Q1: Go 泛型和 Java/TypeScript 泛型有什么区别?

答案

维度GoJavaTypeScript
实现方式编译时单态化 + 字典传递类型擦除编译时擦除
运行时类型信息部分保留❌ 擦除❌ 擦除
约束方式接口(类型集合)extends 上界extends
方法泛型❌ 不支持
协变/逆变通配符 ?in/out

Go 的泛型实现混合了两种策略:对于指针大小的类型用字典传递(共享一份代码),对于值类型可能单态化(生成专用代码)。

Q2: 什么时候应该用泛型?什么时候不应该用?

答案

应该用泛型

  • 通用数据结构(栈、队列、集合、树)
  • 通用算法(排序、查找、过滤、映射)
  • 类型安全的工具函数

不应该用泛型

  • 只有 2-3 种具体类型→直接写具体版本
  • 方法行为随类型差异很大→用接口
  • 只是为了避免 interface{} 的类型断言→可能过度设计
经验法则

"Don't use generics until you find yourself writing the same code three times with different types."

Q3: comparable 约束包含哪些类型?

答案

comparable 包含所有支持 ==!= 的类型:

  • 所有基本类型(bool、int、float、string、complex)
  • 指针、channel
  • 数组(元素可比较的)
  • 结构体(所有字段可比较的)
  • 接口

不包含:slice、map、func。

Q4: 泛型函数是在编译时还是运行时确定类型?

答案

编译时。Go 泛型是静态的,类型在编译时就确定了。Go 编译器使用两种策略:

  1. GC Shape Stenciling:对具有相同 GC 形状的类型共享代码(如所有指针类型共享一份)
  2. 字典传递:通过隐藏的字典参数传递类型信息

这种混合策略平衡了编译速度(不完全单态化)和运行时性能。

相关链接