Go 泛型详解
概述
泛型,将类型看成变量,定义类型约束,使类型约束的所有类型皆具有相关代码,是对类型第二个维度的描述。golang中的泛型称做类型参数(Type Parameter)是在编译时将使用的类型都会具体实现一遍,类型越多就会导致编译速度变慢,编译后文件变大。
泛型的作用
graph TB
A[需要为不同类型实现相同逻辑] --> B{使用泛型前}
A --> C{使用泛型后}
B --> D1[为 int 实现 Add]
B --> D2[为 uint 实现 Add]
B --> D3[为 float 实现 Add]
B --> D4[代码重复
维护困难]
C --> E1["定义泛型函数
Add[T int|uint|float] T"]
E1 --> E2[编译器自动生成
各类型的具体实现]
E2 --> E3[代码复用
类型安全]
style D4 fill:#ffcccc
style E3 fill:#ccffcc
泛型的优势:
- 代码复用:避免为不同类型重复编写相同逻辑
- 类型安全:编译时进行类型检查,避免运行时错误
- 性能优化:编译时生成具体类型代码,无运行时开销
- 更好的抽象:提高代码的可读性和可维护性
函数
1 | // 普通函数 |
与普通的Add函数相比,泛型函数多了对参数和返回值类型的定义:
[]表示声明泛型约束T表示类型形参(type arguments)int|uint、float32|float64表示类型约束,多个类型约束用|隔开,表示只允许类型约束列表中的类型能作为类型实参a K, b V表示类型为K和V的函数形参- 多个类型参数用
,隔开 ~表示底层类型(underlying type),type MyInt int表示类型名MyInt,底层类型为int
泛型函数语法结构
graph LR
A["func Add[T int|uint](a, b T) T"] --> B[函数名]
A --> C["[T int|uint]"]
A --> D["(a, b T)"]
A --> E["T"]
C --> C1[类型参数列表]
C1 --> C2["T: 类型形参"]
C1 --> C3["int|uint: 类型约束"]
D --> D1[函数参数列表]
D1 --> D2["a, b: 参数名"]
D1 --> D3["T: 参数类型"]
E --> E1[返回值类型]
style C fill:#ffcccc
style D fill:#ccffcc
style E fill:#ccccff
语法说明:
- 类型参数列表:
[T Constraint],定义类型参数和约束 - 函数参数:使用类型参数
T作为参数类型 - 返回值:使用类型参数
T作为返回类型
类型推导
类型推导允许调用泛型函数不需要显式传入类型实参,看起来和调用普通函数没有区别
调用泛型函数时,需对类型参数实例化,既可显式写出类型实参,也可由编译器推导,两者等价
1 | Add(1, 2) // 编译器推导为 Add[int](1, 2) |
类型推导流程
flowchart TD
A[调用泛型函数] --> B{是否显式指定类型?}
B -->|是| C[使用显式类型]
B -->|否| D[编译器推导]
D --> E[分析函数参数类型]
E --> F[匹配类型约束]
F --> G{推导成功?}
G -->|是| H[使用推导的类型]
G -->|否| I[编译错误]
C --> J[类型检查]
H --> J
J --> K[生成具体类型代码]
style D fill:#ffcccc
style H fill:#ccffcc
style I fill:#ff9999
类型约束推导,允许多个相关联的类型约束调用时,不需要显式传入类型实参
1 | // 定义切片类型 |
匿名函数
匿名函数不支持泛型,但是支持已经定义好的类型参数
1 | // ❌ 匿名函数不支持泛型 |
类型断言
在泛型函数中不能对类型参数所对应的函数形参进行类型断言
但是支持:
- 先将参数转换成any再进行类型断言
- 支持反射(需要思考是否值得)
- 支持类型转换。
1 | // ❌ a和b是类型为T的Add函数的形参,不允许类型断言 |
结构
声明结构与声明方法类似,在结构名后面添加类型约束
实例化对象时类型必须显式声明,编译器不会推导
1 | // 声明泛型结构 |
结构体声明,允许类型形参嵌套,但不允许循环引用
1 | type Struct[T int|uint|float32|float64, S []T] struct { |
如果泛型约束为子集,则可以嵌套声明type IntSlice[T int|uint] Slice[T],因为IntSlice的类型约束是Slice类型约束的子集,所以允许嵌套声明;如果不是子集type IntSlice[T int|int8] Slice[T]则不允许嵌套声明。
声明示例
1 | // ❌ 类型形参不能单独使用 |
成员方法
成员方法,会对每一个类型约束都实现相应的方法
1 | func (s *Slice[T]) Sum() T {} |
golang不支持泛型方法,但支持使用结构泛型的类型参数
1 | // ❌ 不支持成员方法提供类型参数 |
参数返回值
当泛型结构做参数和返回值时,需要将泛型结构特化成指定类型的结构,也可以使用函数的类型约束
1 | // 特化类型为int,与普通函数没有区别 |
接口
接口用于定义类型约束的集合
1 | type Int interface { |
接口允许嵌套组合,|表示并集,换行表示交集
1 | // 接口可以嵌套 |
接口类型集关系
graph TB
subgraph "并集操作 |"
A1[Int]
A2[Uint]
A3[Float]
A4["Number = Int | Uint | Float"]
A1 --> A4
A2 --> A4
A3 --> A4
end
subgraph "交集操作 换行"
B1[Int]
B2[Uint]
B3[NullNumber = Int & Uint]
B1 --> B3
B2 --> B3
B3 --> B4[空集
不存在这样的类型]
end
style A4 fill:#ffcccc
style B4 fill:#ccffcc
类型集规则:
- 并集(
|):类型可以是其中任意一种 - 交集(换行):类型必须同时满足所有条件
- 空集:如果交集条件无法满足,类型集为空
特殊类型集
any,表示任意类型的集合,是对原有interface{}的替代comparable,表示可以判断是否相等的类型集合,不能与其他类型进行|操作
graph TB
subgraph "any 类型集"
A1[所有类型]
A2[基本类型]
A3[复合类型]
A4[接口类型]
A5[函数类型]
A1 --> A2
A1 --> A3
A1 --> A4
A1 --> A5
end
subgraph "comparable 类型集"
B1[可比较类型]
B2[基本类型
除 slice/map/func]
B3[指针类型]
B4[数组类型
元素可比较]
B5[结构体类型
字段可比较]
B1 --> B2
B1 --> B3
B1 --> B4
B1 --> B5
end
style A1 fill:#ffcccc
style B1 fill:#ccffcc
使用示例:
1 | // any 可以接受任何类型 |
接口函数,允许类型与函数同时定义,并且语义同上,换行表示交集
1 | // Number 表示类型为Int或Uint或Float的并且实现了Add函数的类型 |
接口分类
- 基本接口,表示只有方法约束的接口,
1.18之前的interface - 一般接口,不止包含方法约束还包括类型约束,只能用于类型约束,不能用于变量声明
- 泛型接口,声明时包含类型约束的接口
1 | // 基本接口 |
基本接口(Basic interfaces)
只包含函数约束列表的接口,即原有接口定义形式,兼容原有语义
一般接口(General interfaces)
描述兼容泛型的接口,支持函数约束和类型约束,接口类型集规则如下:
- 空接口的类型集是所有非接口类型的集合,
any = interface{} - 非空接口的类型集是其接口元素的类型集的交集,每行表示
&语义 - 方法的类型集是所有非接口类型拥有的方法集中包含该方法,定义实现接口
- 非接口类型的类型集是仅由类型组成的集合,类型的集合,允许
|操作 ~T形式的项的类型集是底层类型为T的所有类型的集合- 项并集
t1|t2|…|tn是项的类型集的并集
1 | // 定义实现io.ReadWriter且底层类型是[]byte或string或底层类型是Data字段结构的接口 |
一般接口类型集计算
graph TB
subgraph "类型集计算规则"
A1[接口定义] --> A2{接口元素}
A2 --> B1[方法约束
Read/Write]
A2 --> B2["类型约束
~[]byte | ~string"]
B1 --> C1[方法类型集
所有实现该方法的类型]
B2 --> C2[类型约束类型集
底层类型匹配的类型]
C1 --> D[交集 &
同时满足所有约束]
C2 --> D
D --> E[最终类型集]
end
subgraph "示例"
F1["ReadWriter =
io.ReadWriter &
(~[]byte | ~string)"]
F2[必须实现 Read/Write]
F3["底层类型是 []byte 或 string"]
F1 --> F2
F1 --> F3
F2 --> F4[最终类型集]
F3 --> F4
end
style D fill:#ffcccc
style E fill:#ccffcc
style F4 fill:#ccffcc
类型集计算示例:
1 | // 类型集 = (实现 Read 方法的类型) & (实现 Write 方法的类型) & (底层类型为 []byte 或 string 的类型) |
错误示例
1 | type Invalid interface { |
泛型接口
在定义时拥有类型约束,即表示泛型接口
1 | // 定义泛型接口 |
实现接口
类型T实现接口I的条件
- 类型
T不是接口并且是类型集I的元素 - 类型
T是一个接口并且类型集T是类型集I的子集
基本接口
和原有逻辑一样,只要实现了函数约束列表中的所有函数即可实现该接口
1 | // 实现error接口 |
一般接口
一般接口只能用作类型约束不能被实现
1 | // 实现ReadWriter接口 |
泛型接口
需要先确定接口类型参数,让接口转为具体的实例化接口。实例化后如果是基本接口则即可声明变量又可作类型约束,如果实例化后是一般接口则只可做类型约束。
1 | // 定义实现泛型接口类型 |
常见使用场景
容器类型
泛型最常见的用途是实现类型安全的容器:
1 | // 泛型栈 |
工具函数
实现通用的工具函数,避免类型转换:
1 | // 查找切片中的元素 |
约束组合
使用接口组合创建复杂的类型约束:
1 | // 数值类型约束 |
性能考虑
编译时生成
Go 泛型采用**单态化(Monomorphization)**实现:
graph LR
A["泛型函数
Add[T int|uint] T"] --> B[编译时]
B --> C1["Add[int] 具体实现"]
B --> C2["Add[uint] 具体实现"]
C1 --> D[二进制文件]
C2 --> D
style A fill:#ffcccc
style C1 fill:#ccffcc
style C2 fill:#ccffcc
特点:
- 每个使用的类型都会生成一份具体代码
- 运行时无额外开销,性能与手写代码相同
- 编译时间和二进制文件大小会增加
最佳实践
- 合理使用类型约束:不要过度使用
any,尽量使用具体约束 - 避免过度泛型化:不是所有场景都需要泛型,简单场景直接写具体类型
- 注意编译时间:大量使用泛型会增加编译时间
- 类型约束设计:设计良好的类型约束可以提高代码复用性
常见错误和注意事项
类型约束错误
1 | // ❌ 类型约束不匹配 |
类型推导失败
1 | // ❌ 无法推导类型 |
方法不支持泛型
1 | type Stack[T any] struct { |
总结
- golang实现泛型,减少了对不同类型相同的代码
- 利用先约束后使用的策略,明确代码含义
- 支持了对底层数据类型的支持,弥补了对基础类型不能实现方法的不足
- 有类型约束的类型只能做类型约束,不能实例化对象
- 编译时生成具体类型代码,运行时无额外开销
- 合理使用泛型可以提高代码复用性和类型安全性