本文转载自 脑子进煎鱼了,作者 陈煎鱼
经历九九八十一难,多年的不断探讨和 Go 语言爱好者们在社区中的强烈关注,且 Go 官方在 2020 年不断放出消息。
Go 团队核心成员 Ian Lance Taylor 宣布已提交为 Go 添加泛型的提案 (Proposal)。
Ian 在博客说道:“为 Go 添加泛型的语言变更完全向后兼容 (fully backward compatible),现有的 Go 程序会继续像现在一样正常运行。”
基本语法如下:
其大体的概述如下:
-
函数可以具有使用方括号的其他类型参数列表,但其他情况下看起来像普通的参数列表:
func F[T any](p T) { ... }
。 -
类型也可以具有类型参数列表:
type MySlice[T any] []T
。 -
每个类型参数都有一个类型约束,就像每个普通参数都有一个类型:
func F[T Constraint](p T) { ... }
。 -
类型约束是接口类型。
-
新的预声明名称
any
是允许任何类型的类型约束。 -
用作类型约束的接口类型可以具有预先声明的类型的列表。只有与那些类型之一匹配的类型参数才能满足约束条件。
-
泛型函数只能使用其类型约束所允许的操作。
-
使用泛型函数或类型需要传递类型实参。
-
在通常情况下,类型推断允许省略函数调用的类型参数。
根据官方博客的消息,如果该提案被正式接受。那么将会在 2021 年底之前完成一个基本可用的泛型功能(可能未完全优化),又或是会作为 Go1.18beta 的一部分。
这是 Go 泛型特性的又一步历史性前进,这一个里程碑。
为什么 Go 的泛型一拖再拖?
泛型程序设计(generic programming)是程序设计语言的一种风格或范式。泛型允许程序员在强类型语言中编写代码时,使用一些以后才确定的类型,其在真正实例化时才会为这些参数指确定类型。
另外各语言和其编译器、运行环境对泛型的支持均不一样,因此需要针对来辩证。
简单来讲,泛型就是参数化多态。其可根据实参类型生成不同的版本,支持任意数量的调用:
在编译时期编译器便确定其 T 的入参类型。这也是 Go 泛型实现的要求之一 “编译时类型安全”。
为什么需要泛型
这时候可能会有人说,没有泛型也可以啊...感觉写业务代码没什么影响,与其搞泛型不如搞好 errors。
但泛型是有其所需的场景,最常见的是像基础库在处理获取配置中心数据时,就要处理类型,时常遇到下述场景:
手写一个 “泛型” 如果使用接口(interface)类型来做,也得 switch.(type)
枚举出所有的基础类型。这显然并不合理,也没法做太复杂的逻辑,而且所支持的类型还泄露。
另外同时单从语言层面来讲,泛型支持是一个必然事件了,因为泛型的存在对解决特定领域的问题存在一定的意义。
接口和泛型有什么区别
在上面我们有提到接口(interface)类型,这时候就出现了泛型的第二个经典问题。那就是 “接口和泛型有什么区别?”,为什么不用接口来实现 “泛型”:
也像这么一回事,但在这里存在一个致命的缺陷。那就是接口的入参和出参均可以在运行时表现为不同的类型:
要做好,还得依靠内部去对参数进行断言,否则作为 string 类型的煎鱼又如何和 int 类型的 233 相加呢,那是必然报错的。
而反过来看真 “泛型” 的实际使用,编译器会保证泛型函数的入参和出参必须为同一类型,有强制性的检验:
两者存在本质上的区别,泛型会更安全,能够保证编译早期就发现错误,而不是等到运行时(并且可能会存在隐性的 BUG)。
总体来讲,泛型相较接口有如下优点:
-
更安全:编译早期就能发现错误。
-
性能好:静态类型。
过去:为什么那么久都没有泛型
前几段在社区的微信群看到一位小伙伴吐槽 “Go 语言居然没有泛型?”。
变相来看,可能其会认为 ”Go 都已经 11 岁了,2020 年了居然还没有泛型?”。
这显然是不对的,因为泛型本质上并不是绝对的必需品,更不是 Go 语言的早期目标,因此在过往的发展阶段没有过多重视这一点,而是把精力放在了其他 feature 上。
另外 Go 语言在以往其实进行过大量的泛型 proposal 试验,基本时间线(via @changkun)如下:
简述 | 时间 | 作者 |
---|---|---|
[Type Functions] | 2010年 | Ian Lance Taylor |
Generalized Types | 2011年 | Ian Lance Taylor |
Generalized Types v2 | 2013年 | Ian Lance Taylor |
Type Parameters | 2013年 | Ian Lance Taylor |
go:generate | 2014年 | Rob Pike |
First Class Types | 2015年 | Bryan C.Mills |
Contracts | 2018年 | Ian Lance Taylor, Robert Griesemer |
Contracts | 2019年 | Ian Lance Taylor, Robert Griesemer |
Redundancy in Contracts(2019)'s Design | 2019年 | Ian Lance Taylor, Robert Griesemer |
Constrained Type Parameters(2020, v1) | 2020年 | Ian Lance Taylor, Robert Griesemer |
Constrained Type Parameters(2020, v2) | 2020年 | Ian Lance Taylor, Robert Griesemer |
Constrained Type Parameters(2020, v3) | 2020年 | Ian Lance Taylor, Robert Griesemer |
虽然偶有中断,但仔细一看,2010 年就尝试过,到 2020 年了,也是很励志了,显然官方也是在寻路和尝试的过程中,但一直没有找到相较好的方案,大多都存在问题,社区对方案的争议也不断。
泛型的战争
为什么不用尖括号
在社区中很多同学在讨论的一个问题,那就是 “为什么 Go 泛型不像 C++ 和 Java 那样使用尖括号?,也出现了 “Go 一直标榜业界工程实践类的榜样,为什么就是不用尖括号” 的言论?
思考问题我们不只看表面,官方说不行,那么我们可以倒推来看,看看 Go 语言就用尖括号:
普通的函数声明看上去似乎结构清晰,没有什么大问题的。接着往下看:
我们继续把代码演进一下,简洁一点:
这时候就犯难了,不仅编译器难以解析,人也很难判别,到底指的是:
又或是:
从上述代码来看,使用尖括号难以分别,因为没有类型信息,就无法确定赋值的右侧是一对表达式 w < x和y > (z)
,还是返回两个结果值 w<x, y>(z)
的泛型函数实例化和调用,其存在歧义。
要解决还要引入新的约束,会破坏 Go1 的兼容性承诺,这显然是不合理的。
为什么不用括号
其实最早 Go 泛型的版本是使用了括号的模式,虽然能用,但是用括号会引入新的解析歧义。例如:
从语法上来讲,你无法识别他是未命名参数的 x(T)
函数,还是类型名为参数的 (T)
函数。
同时 Go 语言还存在强制类型转换这一语法,假设代码是 []T(v1)
和 []T(v2){}
,那么你在开括号处,就无法得知其是否代表类型转换。
甚至在函数的完整声明上,我们都会感到困惑:
函数入参、泛型、返回值声明均都是括号,造成了语义不清,这显然也是不合理的。
为什么不用书名号(«»)
想的美,并不想使用非 ASCII,未来更没打算支持。
总结
最后我们回答一下最开始的疑问,”为什么 Go 的泛型一拖再拖“,主要如下:
-
Go 语言的早期目标(工作重点)并不是泛型。
-
Go 语言在 2010-2020 年都有间断在做 Go 泛型的 proposal,但总是 ”失败“,在不断地吸收经验。
-
Go 语言社区的意见反馈是真的多,单用什么符号表示泛型,不想要泛型都争论不休。
-
Go 语言的泛型现在还不成熟,很多细节其实并没有支持好。
很显然,在保证 Go1 向后兼容性的同时,Go 官方也不想直接妥协出一个随便的方案,因此总是不断地在改进。
随着 Go 语言的在业内的不断应用,泛型也和 errors 一样被推上风头浪尖。