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

218 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [go, golang, 泛型, Generics]
create time: 2026-06-07 15:15
---
# 泛型
## 概述
Go 1.18 引入的泛型是语言自发布以来最重要的更新。它允许编写与具体类型无关的通用代码,同时保留编译期类型检查的安全性。本文从动机、语法到约束机制全面讲解 Go 泛型。
## 正文
### 为什么需要泛型?
> [!question] 💭 思考
> 如果需要一个函数能同时处理 `[]int`、`[]float64`、`[]string`,在 Go 1.17 之前你会怎么做?
没有泛型时,只能为每种类型写一个重复的函数:
```go
// ❌ 没有泛型:每种类型都需要一个函数
func sumInts([]int) int { ... }
func sumFloats([]float64) float64 { ... }
func sumStrings([]string) string { ... } // 逻辑完全相同!
```
用反射可以解决但代价高昂:
```go
// ❌ 反射方案:运行时开销大 + 失去编译期类型检查
func sum(v interface{}) interface{} {
rv := reflect.ValueOf(v)
// ... 各种 Kind 判断和类型断言
}
```
> [!info] ℹ️ 泛型的优势
> - **消除重复代码**:一套逻辑适配多种类型
> - **编译期检查**:类型错误在编译时发现,而非运行时 panic
> - **零运行时开销**:泛型代码会被实例化为具体类型的机器码
### 基本语法
#### 类型参数
```go
// [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) // 显式指定(通常不需要)
```
方括号 `[...]` 用于类型参数列表,圆括号 `(...)` 用于值参数列表——这是最容易混淆的地方。
```mermaid
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
```
#### 类型约束
约束定义了一个"允许的类型集合":
```go
// 方式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 别名
```go
// 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] }
```
### 泛型类型
泛型不仅适用于函数,也适用于结构体:
```go
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 支持两种类型推断:
#### 函数参数推断
```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] 📝 推断的限制
> 当类型参数仅出现在返回值中时,无法推断:
> ```go
> func NewItem[T any]() T { /* ... */ }
> // NewItem() ❌ 无法推断 T
> // NewItem[int]() ✅ 必须显式指定
> ```
#### 约束推断
```go
// 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] 💡 组合使用效果最佳
> ```go
> // 接口定义行为
> 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语言接口]] — 接口的回顾