Go - Generics
泛型概念
将
算法
与类型
解耦,实现算法更广泛的复用
实现方向
- 拖慢
程序员
- 不实现泛型,不会引入复杂性
- 但需要程序员花费精力重复实现
同逻辑但不同类型
的函数或者方法
- 拖慢
编译器
– C++/Go- 类似
C++
的泛型实现方案,通过增加编译器的负担为每个类型实例生成一份单独
的泛型函数的实现 - 产生大量的代码,且大部分是多余的
- 类似
- 拖慢
执行性能
– Java- 类似
Java
的泛型实现方案,即伪泛型
,通过隐式的装箱
和拆箱
操作消除类型的差异 - 虽然节省了空间,但代码执行效率低
- 类似
泛型设计
类型系统
静态强类型
:C++/Java/Go;动态强类型:Python;动态弱类型:JavaScript
- Go 为强类型语言,而 JavaScript 为弱类型语言,Go 的
类型强度
高于 JavaScript - Go 和 Python 都有较高的类型强度,但
类型检查
的时机不同Go
是在编译期
,而Python
则在运行期
- 如果
类型检查
是发生在运行期
,则为动态类型语言
动态类型
不仅仅表现在变量的类型
可以更改,在 OOP 的编程语言中,类的定义
也可以动态修改
- 动态类型的优点
- 动态类型有更好的
灵活性
,可以在运行时
修改变量的类型
和类的定义
,容易实现热更新
- 编写方便,适用于
小规模脚本
- 动态类型有更好的
- 动态类型的缺点
代码晦涩,难以维护
- 一般没有类型标注,即便有类型标注,也可以在运行时修改
性能差
- 因为在
编译期
缺少类型提示,编译器无法为对象安排合理的内存布局
- 因此 Python 和 JavaScript 的对象布局比 Java/C++ 等静态类型语言会
更复杂
,且带来性能下降
- 因为在
Python 为
动态强类型
语言,运行时报错
1 | 1 a = |
1 | class A(): |
Go 为
静态强类型
语言,编译报错
1 | var a = 1 |
CPP
1 |
|
- 在 C++ 中,泛型类型被翻译成机器码的时候,
创建
了不同的类型 -真泛型
- 使用类型得到
新类型
泛型声明
:输入参数是类型,返回值也是类型的函数
Java
- 库分发:将源码先翻译成字节码,然后将字节码打包成 jar 包,然后在网络上进行分发
- Java 的泛型设计采用的是
泛型擦除
的方式来实现 -伪泛型
1 | ArrayList<Integer> a = new ArrayList<>(); |
1 | // java: name clash: sayHello(java.util.ArrayList<java.lang.Integer>) and sayHello(java.util.ArrayList<java.lang.String>) have the same erasure |
Type Set
概念
- 每个
类型
都有一个 type set 非接口
类型的类型的 type set 中仅包含其自身
- 例如非接口类型
T
,其 type set 中唯一的元素为{T}
- 例如非接口类型
- 对于一个普通的
接口
类型来说,其 type set 是无限集合
- 所有
实现
了该接口类型所有
方法的类型,都是该集合的一个元素
- 此接口类型
自身
也是 type set 的一个元素
- 所有
- 空接口类型
interface{}
的 type set 中包含了所有
可能的类型
表述
- 当类型 T 是接口类型 I 的 type set 的
元素
时,类型 T实现
了 接口 I - 使用
嵌入
接口类型组合
而成的接口类型,其 type set 为其所有嵌入的接口类型的 type set 的交集
1 | type E1 interface { |
MyInterface3 的 type set 为 E1、E2 和
type E3 interface { MyMethod3() }
的 type set 的交集
基本语法
类型参数
Type parameter
- 类型参数是在
函数声明
、方法声明
的 receiver 部分、类型定义
的类型参数列表,声明的(非限定
)类型名称 - 类型参数在声明中充当一个
未知类型
的占位符,在泛型函数或者泛型类型实例化
时,类型参数会被一个类型实参替换
普通函数
1 | func Foo(x, y aType, z bType) |
泛型函数
1 | // [...] 类型参数列表,不支持变长类型参数 |
约束
Constraint -
type set
+方法集合
约束
规定一个类型参数
必须满足的条件要求- 如果某个类型满足了某个约束规定的
所有
条件要求,则该类型是这个约束修饰的类型形参的一个合法
的类型实参
使用
interface
类型来定义约束
1 | package main |
建议:将做
约束
的接口类型和做方法集合
的接口类型分开定义
泛型函数
与
C++
类似
Instantiation
1 | func Sort[Elem interface{ Less(y Elem) bool }](list []Elem) {} |
具化 -
Instantiation
Sort[book]
,发现要排序的对象类型为book
- 检查 book 类型是否满足
约束
要求,即是否实现约定定义中的 Less 方法- 如果满足,将其作为类型实参
替换
Sort 函数中的类型形参,即Sort[book]
- 如果不满足,则
编译报错
- 如果满足,将其作为类型实参
- 将泛型函数 Sort
具化
为一个新函数
,其函数原型为func([]book)
booksort := Sort[book]
调用 -
Invocation
- 调用具化后的
泛型函数
,与普通的函数调用没有区别 - 即
booksort(books)
- 只需要检查传入的函数实参 books 的类型与 booksort 的函数原型中的形参类型
[]book
是否匹配
- 只需要检查传入的函数实参 books 的类型与 booksort 的函数原型中的形参类型
过程
1 | Sort[book](books) |
泛型类型
1 | // 带有类型参数的类型定义 |
1 | // any is an alias for interface{} and is equivalent to interface{} in all ways. |
同样遵循顺序:
先具化,再使用
1 | type Vector[T any] []T |
泛型性能
与
C++
类似,以牺牲编译效率
为代价
- Go 泛型没有拖慢程序员的
开发效率
,也没有拖慢运行效率
- Go 1.18
编译器
的性能比 Go 1.17下降 15%
,将在 Go 1.19 得到改善,抵消 Go 泛型带来的影响
使用建议
适用
容器类
:函数的操作元素的类型为slice
、map
、channel
等特定类型通用
数据结构(如链表、二叉树),即像 slice 和 map 一样,但 Go 又没有提供原生支持的类型- 使用
类型参数
替代接口类型,避免进行类型断言
,并且可以在编译
阶段进行全面的类型静态检查
不适用
对某一类型的值的
全部操作
,都只是在那个值上调用一个方法
,应该使用接口类型
,而非参数类型
1 | type Reader interface { |
1 | func ReadAll[reader io.Reader](r reader) ([]byte, error) // 错误用法,使得代码更复杂 |
当不同类型使用一个共同的方法,且该方法的
实现
对所有类型都相同
,应该使用类型参数
,否则不能使用,如 Sort
在多次编写
完全相同
的样板代码,差异仅仅是类型
,可以考虑使用类型参数
,否则不要急于使用类型参数
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.