泛型
问题
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 引入了 slices 和 maps 泛型包:
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 泛型有什么区别?
答案:
| 维度 | Go | Java | TypeScript |
|---|---|---|---|
| 实现方式 | 编译时单态化 + 字典传递 | 类型擦除 | 编译时擦除 |
| 运行时类型信息 | 部分保留 | ❌ 擦除 | ❌ 擦除 |
| 约束方式 | 接口(类型集合) | 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 编译器使用两种策略:
- GC Shape Stenciling:对具有相同 GC 形状的类型共享代码(如所有指针类型共享一份)
- 字典传递:通过隐藏的字典参数传递类型信息
这种混合策略平衡了编译速度(不完全单态化)和运行时性能。