Files
cs-note/hzh/GolangStar/Go语言进阶/范型.md
T

5.6 KiB
Raw Blame History

tags, create time
tags create time
go
golang
泛型
Generics
2026-06-07 15:15

泛型

概述

Go 1.18 引入的泛型是语言自发布以来最重要的更新。它允许编写与具体类型无关的通用代码,同时保留编译期类型检查的安全性。本文从动机、语法到约束机制全面讲解 Go 泛型。

正文

为什么需要泛型?

[!question] 💭 思考 如果需要一个函数能同时处理 []int、[]float64、[]string,在 Go 1.17 之前你会怎么做?

没有泛型时,只能为每种类型写一个重复的函数:

// ❌ 没有泛型:每种类型都需要一个函数
func sumInts([]int) int { ... }
func sumFloats([]float64) float64 { ... }
func sumStrings([]string) string { ... } // 逻辑完全相同!

用反射可以解决但代价高昂:

// ❌ 反射方案:运行时开销大 + 失去编译期类型检查
func sum(v interface{}) interface{} {
    rv := reflect.ValueOf(v)
    // ... 各种 Kind 判断和类型断言
}

[!info] ℹ️ 泛型的优势

  • 消除重复代码:一套逻辑适配多种类型
  • 编译期检查:类型错误在编译时发现,而非运行时 panic
  • 零运行时开销:泛型代码会被实例化为具体类型的机器码

基本语法

类型参数

// [T int | float64] 声明类型参数 T,约束为 int 或 float64
func Max[T int | float64](a, b T) T {
    if a > b {
        return a
    }
    return b
}

// 调用(可省略类型参数,编译器自动推断)
m1 := Max(3, 5)            // T = int
m2 := Max(2.5, 3.5)        // T = float64
m3 := Max[int](3, 5)       // 显式指定(通常不需要)

方括号 [...] 用于类型参数列表,圆括号 (...) 用于值参数列表——这是最容易混淆的地方。

flowchart LR
    A["func Max[T int|float64]<br/>(a, b T)<br/>T"] --> B["[T int|float64]<br/>类型参数列表"]
    A --> C["(a, b T)<br/>值参数列表"]
    A --> D["T<br/>返回类型"]
    
    style B fill:#e3f2fd
    style C fill:#fff3e0
    style D fill:#e8f5e9

类型约束

约束定义了一个"允许的类型集合":

// 方式1:内联约束
func Process[T int | string | bool](v T) {}

// 方式2:预定义约束接口(推荐,可复用)
type Number interface {
    ~int | ~float64 | ~int64
}

func SumSlice[S ~[]E, E Number](s S) E {
    var total E
    for _, v := range s {
        total += v
    }
    return total
}

[!tip] 💡 理解约束中的 ~ 符号

  • int:只匹配 int 本身
  • ~int:匹配所有底层类型为 int 的类型(包括 type MyInt int)
  • ~[]E:匹配所有底层类型为切片且元素类型为 E 的类型

any 别名

// any 就是 interface{} 的别名
type any = interface{}

// 以下两种写法等价
func First[T any](s []T) T { return s[0] }
func First[T interface{}](s []T) T { return s[0] }

泛型类型

泛型不仅适用于函数,也适用于结构体:

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
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s)-1]
    return item, true
}

// 使用
intStack := Stack[int]{}
intStack.Push(42)

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

[!warning] ⚠️ 常见陷阱

  1. 方法接收者必须是泛型类型本身——不能给非泛型类型添加泛型方法
  2. 类型参数名只需在函数/类型内部一致——func Max[T comparable](a, b T) 中参数名 T 可以换成任意字母
  3. 单个类型参数的约束只有一个成员时,末尾逗号可选但建议加上以消除歧义

类型推断

Go 支持两种类型推断:

函数参数推断

func Swap[T any](a, b T) (T, T) { return b, a }

var x, y int = 1, 2
x, y = Swap(x, y) // ✅ 编译器推断 T = int,无需 Swap[int](x, y)

[!note] 📝 推断的限制 当类型参数仅出现在返回值中时,无法推断:

func NewItem[T any]() T { /* ... */ }
// NewItem() ❌ 无法推断 T
// NewItem[int]() ✅ 必须显式指定

约束推断

// S 的约束是 ~[]E,当知道 S = Vector ([]int32),可推断 E = int32
func MultiplyEach[S ~[]E, E constraints.Integer](s S, factor E) S {
    result := make(S, len(s))
    for i, v := range s {
        result[i] = v * factor
    }
    return result
}

type Vector []int32
v := Vector{1, 2, 3}
result := MultiplyEach(v, 3) // S=Vector, E=int32,全部自动推断

何时使用泛型 vs 接口?

[!question] 💭 思考 面对"既要灵活又要类型安全"的需求,该选泛型还是接口?

场景 推荐方案 原因
定义行为契约(如 Reader/Writer) 接口 关注能力而非类型
通用算法(排序、查找、集合操作) 泛型 操作的是数据本身
插件/扩展点架构 接口 使用者实现接口即可
容器类型(List、Map、Tree) 泛型 需要持有任意类型的元素

[!tip] 💡 组合使用效果最佳

// 接口定义行为
type Sortable interface {
    Len() int
    Less(i, j int) bool
    Swap(i, j int)
}

// 泛型实现通用排序算法
func Sort[S ~[]E, E any](s S, less func(E, E) bool) {
    // 通用排序逻辑
}

关联笔记