5.6 KiB
5.6 KiB
tags, create time
| tags | create time | ||||
|---|---|---|---|---|---|
|
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] ⚠️ 常见陷阱
- 方法接收者必须是泛型类型本身——不能给非泛型类型添加泛型方法
- 类型参数名只需在函数/类型内部一致——
func Max[T comparable](a, b T)中参数名 T 可以换成任意字母- 单个类型参数的约束只有一个成员时,末尾逗号可选但建议加上以消除歧义
类型推断
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) { // 通用排序逻辑 }
关联笔记
- hzh/GolangStar/Go语言进阶/反射 — 泛型可以在编译期解决的问题,不要用反射在运行时解决
- hzh/GolangStar/Go语言基础/Go语言接口 — 接口的回顾