Skip to content

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.OrderedInteger | Float | ~string(可排序)
constraints.Integer所有有符号和无符号整数
constraints.Floatfloat32, float64
constraints.Signedint, int8, int16, int32, int64
constraints.Unsigneduint, uint8, uint16, uint32, uint64, uintptr
constraints.Complexcomplex64, 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&lt;int&gt; → 生成一份代码<br/>vector&lt;double&gt; → 再生成一份代码"]
        C2["✅ 零运行时开销<br/>✅ 每种类型独立优化"]
        C3["❌ 代码膨胀 (二进制体积大)"]
        C4["❌ 编译错误信息晦涩"]
    end

    subgraph Java["Java 泛型 (Type Erasure)"]
        J1["编译期: List&lt;Integer&gt; → List&lt;Object&gt;<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 秒才看懂,那就该简化。


参考 ​

批注模式

💬 文章评论

暂无评论,来说点什么吧 👇

编程学习笔记