Go 泛型
#golang · #泛型 · #类型参数 · #约束 · #Go1.18
Go 1.18 引入泛型,允许编写类型安全且可复用的代码。泛型的核心是类型参数(Type Parameter)和类型约束(Type Constraint)。不同于 C++ 模板的编译期代码膨胀,Go 泛型采用基于字典的运行时实现,在代码复用和编译性能之间取得平衡。
1. 基础语法
1.1 类型参数
go
// 泛型函数:类型参数放在方括号中
func Max[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
// 泛型结构体
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
}1.2 使用
go
// 显式指定类型参数
maxInt := Max[int](3, 5)
// 类型推断
maxFloat := Max(3.14, 2.71) // Go 自动推断为 float64
// 泛型结构体
intStack := Stack[int]{}
intStack.Push(42)
stringStack := Stack[string]{}
stringStack.Push("hello")2. 类型约束(Constraints)
2.1 内置约束
go
// any = interface{}(空约束,任何类型)
func Print[T any](v T) { fmt.Println(v) }
// comparable = 可比较(支持 == 和 !=)
func Contains[T comparable](slice []T, elem T) bool {
for _, v := range slice {
if v == elem {
return true
}
}
return false
}2.2 标准库约束(golang.org/x/exp/constraints)
| 约束 | 包含的类型 |
|---|---|
constraints.Ordered | Integer | Float | ~string(可排序) |
constraints.Integer | 所有有符号和无符号整数 |
constraints.Float | float32, float64 |
constraints.Signed | int, int8, int16, int32, int64 |
constraints.Unsigned | uint, uint8, uint16, uint32, uint64, uintptr |
constraints.Complex | complex64, complex128 |
Go 1.21+ 将 Ordered 等移入标准库 cmp 包:
go
import "cmp"
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}2.3 自定义约束
go
// 接口约束
type Number interface {
~int | ~int64 | ~float64 // ~ 表示包括底层类型
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
// ~ 的作用:允许自定义类型
type MyInt int
var nums = []MyInt{1, 2, 3}
total := Sum(nums) // 如果没有 ~,这里编译错误2.4 联合类型约束
go
// 多个类型用 | 连接
type Stringer interface {
~string | ~[]byte
String() string
}
// 也可以混合方法约束和类型约束
type Reader interface {
io.Reader
~string | ~[]byte
}3. 进阶用法
3.1 泛型 Map/Filter/Reduce
go
// Map: 对每个元素应用 fn
func Map[T, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
// Filter: 保留满足条件的元素
func Filter[T any](slice []T, fn func(T) bool) []T {
result := make([]T, 0, len(slice))
for _, v := range slice {
if fn(v) {
result = append(result, v)
}
}
return result
}
// Reduce: 聚合
func Reduce[T, U any](slice []T, init U, fn func(U, T) U) U {
result := init
for _, v := range slice {
result = fn(result, v)
}
return result
}
// 使用
nums := []int{1, 2, 3, 4, 5}
doubled := Map(nums, func(n int) int { return n * 2 })
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
sum := Reduce(nums, 0, func(acc, n int) int { return acc + n })3.2 泛型 Set
go
type Set[T comparable] struct {
data map[T]struct{}
}
func NewSet[T comparable](items ...T) *Set[T] {
s := &Set[T]{data: make(map[T]struct{})}
for _, item := range items {
s.Add(item)
}
return s
}
func (s *Set[T]) Add(item T) { s.data[item] = struct{}{} }
func (s *Set[T]) Remove(item T) { delete(s.data, item) }
func (s *Set[T]) Contains(item T) bool {
_, ok := s.data[item]
return ok
}
func (s *Set[T]) Len() int { return len(s.data) }
// 并集:先加 s1 再加 s2
func Union[T comparable](s1, s2 *Set[T]) *Set[T] {
result := NewSet[T]()
for k := range s1.data { result.Add(k) }
for k := range s2.data { result.Add(k) }
return result
}3.3 泛型 Sync Map
go
type SyncMap[K comparable, V any] struct {
mu sync.RWMutex
m map[K]V
}
func NewSyncMap[K comparable, V any]() *SyncMap[K, V] {
return &SyncMap[K, V]{m: make(map[K]V)}
}
func (sm *SyncMap[K, V]) Load(key K) (V, bool) {
sm.mu.RLock()
defer sm.mu.RUnlock()
v, ok := sm.m[key]
return v, ok
}
func (sm *SyncMap[K, V]) Store(key K, value V) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.m[key] = value
}4. 底层实现 — 与 Java/C++ 的本质差异
三种"泛型"看起来相似,但底层实现截然不同——这对性能和二进制大小有直接影响:
mermaid
flowchart TB
subgraph Cpp["C++ 模板 (Template Instatiation)"]
C1["编译期: vector<int> → 生成一份代码<br/>vector<double> → 再生成一份代码"]
C2["✅ 零运行时开销<br/>✅ 每种类型独立优化"]
C3["❌ 代码膨胀 (二进制体积大)"]
C4["❌ 编译错误信息晦涩"]
end
subgraph Java["Java 泛型 (Type Erasure)"]
J1["编译期: List<Integer> → List<Object><br/>擦除类型信息,插入强制转换"]
J2["✅ 二进制兼容(老JVM可运行)"]
J3["❌ 运行时拿不到泛型类型"]
J4["❌ 基础类型需装箱 (Integer不是int)"]
end
subgraph Go["Go 泛型 (GC Shape / Dictionary)"]
G1["所有指针类型共享一份代码"]
G2["✅ 类型安全(无擦除)"]
G3["🟡 少量运行时字典开销"]
G4["✅ 二进制体积不膨胀"]
end| 维度 | C++ 模板 | Java 泛型 | Go 泛型 |
|---|---|---|---|
| 实现 | 编译期代码生成 | 类型擦除 + 强制转换 | GC Shape + 字典传递 |
| 运行时开销 | ❌ 零 | ❌ 装箱拆箱(基础类型) | 🟡 字典查表(~5%) |
| 二进制膨胀 | ❌ 严重(N 种类型 = N 份) | ✅ 无 | ✅ 无(同 shape 共享) |
| 基础类型支持 | ✅ | ❌ 需装箱 Integer | ✅ 直接支持 |
| 运行时反射泛型 | ❌ 编译后丢失 | ❌ 已擦除 | ✅ 字典包含类型信息 |
| 编译速度 | ❌ 慢 | ✅ 快 | 🟡 中等 |
| 错误信息 | ❌ 极长且难读 | 🟡 中等 | ✅ 清晰 |
性能开销实测
go
// 基准测试:泛型 Add vs 具体类型 Add
// Benchmark: go test -bench=. -benchmem
func AddInt(a, b int) int { return a + b }
func AddGeneric[T int](a, b T) T { return a + b }
// 结果(近似):
// BenchmarkAddInt-8 1000000000 0.3 ns/op 0 B/op 0 allocs/op
// BenchmarkAddGeneric-8 800000000 1.5 ns/op 0 B/op 0 allocs/op
// 泛型版慢 ~5x,因为要查字典拿到 > 操作符的函数指针
// 但在实际业务中(Map/Filter/Reduce),
// 泛型的额外开销被业务逻辑淹没,几乎不可测量结论:Go 泛型的运行时开销在微基准测试中可见(~5-10ns),但在实际业务代码中通常被更大的 I/O、网络、内存操作淹没。如果你在写标准库级的通用容器——泛型值得用;如果只是避免写 3 个不同 int 类型的 Max——复制粘贴也不丢人。
4.3 为什么不能用 var x T 直接创建变量
go
func New[T any]() T {
var x T // ✅ 可以声明零值变量
return x
}
func NewSlice[T any]() {
items := make([]T, 10) // ✅ make 可以用
}
// ❌ 不能用 new(T):编译器不知道 T 的大小
// ❌ 不能直接做类型断言:v.(T) 不合法
// ✅ 但可以用接口转换:v.(interface{}) → 反射4.4 泛型与接口的对比
go
// 接口版本:性能有 overhead(装箱、反射)
type Set interface {
Add(interface{})
Contains(interface{}) bool
}
// 泛型版本:编译期确定类型,零开销
type Set[T comparable] struct { ... }| 泛型 | 接口 | |
|---|---|---|
| 类型安全 | 编译期检查 ✅ | 运行时检查 |
| 性能 | 无装箱开销 ✅ | 装箱 + 动态分发 |
| 灵活性 | 编译时确定 | 运行时多态 |
| 代码量 | 无膨胀(字典共享) | 无膨胀 |
5. 常见陷阱
5.1 方法不能有类型参数
go
// ❌ 编译错误
func (s *Stack[T]) Push[U any](item U) {}
// ✅ 正确:方法只能使用类型定义的类型参数
func (s *Stack[T]) Push(item T) {}5.2 不能对类型参数直接使用运算符
go
// ❌ 编译错误:T 不一定支持 + 操作
func Add[T any](a, b T) T { return a + b }
// ✅ 正确:通过约束限制
func Add[T Number](a, b T) T { return a + b }5.3 接口断言限制
go
func Print[T any](v T) {
// ❌ 不能直接做类型断言 T(concreteType)
// ✅ 转为 interface{} 后可以
if s, ok := any(v).(string); ok {
fmt.Println(s)
}
}5.4 泛型与 nil
go
// ❌ 不能将 nil 赋值给泛型变量
func Zero[T any]() T {
return nil // 编译错误
}
// ✅ 使用 var 获取零值
func Zero[T any]() T {
var zero T
return zero
}6. 泛型数据结构实战
6.1 泛型 Set
go
type Set[T comparable] struct {
m map[T]struct{}
}
func NewSet[T comparable]() *Set[T] {
return &Set[T]{m: make(map[T]struct{})}
}
func (s *Set[T]) Add(v T) { s.m[v] = struct{}{} }
func (s *Set[T]) Remove(v T) { delete(s.m, v) }
func (s *Set[T]) Contains(v T) bool {
_, ok := s.m[v]
return ok
}
func (s *Set[T]) Len() int { return len(s.m) }
func (s *Set[T]) Items() []T {
items := make([]T, 0, len(s.m))
for k := range s.m { items = append(items, k) }
return items
}
// 并集
func (s *Set[T]) Union(other *Set[T]) *Set[T] {
result := NewSet[T]()
for k := range s.m { result.m[k] = struct{}{} }
for k := range other.m { result.m[k] = struct{}{} }
return result
}
// 交集
func (s *Set[T]) Intersect(other *Set[T]) *Set[T] {
result := NewSet[T]()
for k := range s.m {
if other.Contains(k) { result.m[k] = struct{}{} }
}
return result
}
// 使用示例
func Example() {
set := NewSet[int]()
set.Add(1)
set.Add(2)
set.Add(2) // 重复,不生效
fmt.Println(set.Len()) // 2
fmt.Println(set.Contains(1)) // true
}6.2 泛型有序 Map(OrderedMap)
go
// OrderedMap: 保持插入顺序的泛型 Map
type OrderedMap[K comparable, V any] struct {
keys []K
values map[K]V
}
func NewOrderedMap[K comparable, V any]() *OrderedMap[K, V] {
return &OrderedMap[K, V]{
keys: make([]K, 0),
values: make(map[K]V),
}
}
func (om *OrderedMap[K, V]) Set(key K, value V) {
if _, exists := om.values[key]; !exists {
om.keys = append(om.keys, key)
}
om.values[key] = value
}
func (om *OrderedMap[K, V]) Get(key K) (V, bool) {
v, ok := om.values[key]
return v, ok
}
// 按插入顺序遍历
func (om *OrderedMap[K, V]) Range(fn func(K, V) bool) {
for _, k := range om.keys {
if !fn(k, om.values[k]) { return }
}
}6.3 泛型 LRU Cache
go
type LRUCache[K comparable, V any] struct {
cap int
cache map[K]*list.Element
list *list.List
}
type entry[K comparable, V any] struct {
key K
value V
}
func NewLRUCache[K comparable, V any](capacity int) *LRUCache[K, V] {
return &LRUCache[K, V]{
cap: capacity,
cache: make(map[K]*list.Element),
list: list.New(),
}
}
func (c *LRUCache[K, V]) Get(key K) (V, bool) {
if ele, ok := c.cache[key]; ok {
c.list.MoveToFront(ele)
return ele.Value.(*entry[K, V]).value, true
}
var zero V
return zero, false
}
func (c *LRUCache[K, V]) Put(key K, value V) {
if ele, ok := c.cache[key]; ok {
ele.Value.(*entry[K, V]).value = value
c.list.MoveToFront(ele)
return
}
if c.list.Len() >= c.cap {
oldest := c.list.Back()
c.list.Remove(oldest)
delete(c.cache, oldest.Value.(*entry[K, V]).key)
}
ele := c.list.PushFront(&entry[K, V]{key, value})
c.cache[key] = ele
}泛型 LRU 的好处:一个实现搞定
LRUCache[string, *User]、LRUCache[int, string],零运行时开销(编译期确定类型+内联)。
7. 何时使用泛型
✅ 适合使用
- 通用容器:Set, Stack, Queue, OrderedMap
- 工具函数:Map, Filter, Reduce
- 缓存抽象:泛型 LRU, 泛型 Pool
- 数学/算法:Max, Min, Sort, BinarySearch
❌ 不推荐使用
- 仅有少量具体类型 → 直接写具体实现
- 可以简单用接口替代 → 优先用接口
- 方法已在接口中定义好 → 不需要泛型
- 过于复杂的约束 → 降低可读性
原则
"当你不确定是否需要泛型时,先不用。等代码出现明显的重复模式时,再用泛型重构。" — Ian Lance Taylor
泛型 vs 接口 vs 代码生成:选型决策树
mermaid
flowchart TD
Q["需要处理多种类型的代码复用"] --> Q1{"类型集合明确且小 (≤5)?"}
Q1 -->|"是"| Q2{"操作主要是方法调用?"}
Q2 -->|"是"| I1["✅ 接口 (interface)<br/>最自然、最简单"]
Q2 -->|"否, 操作是算术/比较等"| G1["✅ 泛型 (generics)<br/>类型安全 + 零装箱"]
Q1 -->|"否, 类型无限/很多"| Q3{"需要编译期类型安全?"}
Q3 -->|"是"| G2["✅ 泛型 (generics)<br/>用 constraints 限定"]
Q3 -->|"否"| Q4{"性能敏感?"}
Q4 -->|"是"| CG["✅ 代码生成 (go generate)<br/>完全静态化,零开销"]
Q4 -->|"否"| I2["✅ 接口 (interface)<br/>+ any 类型断言"]三选对比表
| 维度 | 泛型 (Generics) | 接口 (Interface) | 代码生成 (go generate) |
|---|---|---|---|
| 类型安全 | ✅ 编译期 | ✅ 编译期 (不含 any) | ✅ 编译期 |
| 装箱开销 | ❌ 无 (单态化后) | ✅ 有 (interface 装箱) | ❌ 无 |
| 二进制大小 | ✅ 无膨胀 (字典共享) | ✅ 无膨胀 | ❌ 膨胀 (每类型一份代码) |
| 代码可读性 | 🟡 中等 (类型参数多时复杂) | ✅ 最好 | 🟡 生成代码难阅读 |
| IDE 支持 | ✅ 完整 | ✅ 完整 | ❌ 生成的代码无智能提示 |
| 适用类型数 | 任意 (通过约束) | 任意 (实现接口即可) | 固定 (生成时确定) |
| 调试难度 | 🟡 中等 | ✅ 容易 | ❌ 难 (调试生成代码) |
典型场景决策
| 场景 | 推荐 | 原因 |
|---|---|---|
Sort[T], Max[T], Min[T] | 泛型 | 类型安全 + 零开销 |
Cache[T], Set[T], Stack[T] | 泛型 | 编译期保证类型正确 |
http.Handler | 接口 | 行业标准,扩展性最好 |
io.Reader/Writer | 接口 | 不同的实现,同一个接口 |
| JSON/Protobuf 序列化代码 | 代码生成 | 需要针对每个 struct 生成优化代码 |
| ORM model 代码 | 代码生成 | 每个表结构不同 |
| 低层次性能热点 | 代码生成 | 消除所有抽象层开销 |
反例:什么时候不该用泛型
go
// ❌ 坏:类型参数太多,可读性崩溃
func Process[A any, B constraints.Ordered, C interface{ ~[]B }](a A, b B, c C) (D, error) { ... }
// ❌ 坏:用泛型包装接口
type Reader[T io.Reader] struct { r T } // 直接存 io.Reader 就够了
// ❌ 坏:泛型暴露到 API 边界
// API handler 的参数应该是具体类型或接口,不要用泛型
// ✅ 好:泛型封装常见模式
func Must[T any](v T, err error) T {
if err != nil { panic(err) }
return v
}核心原则:泛型是用来消除重复代码的,不是用来炫耀类型体操的。如果一段泛型代码让你停下来想了 10 秒才看懂,那就该简化。
登录后即可发表评论 👇