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 | a = 1 |
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.











