原创分享 Go 泛型语法又出 “幺蛾子”:引入 type set 概念和移除 type list 中的 type 关键字

bigwhite-github · 2021年04月08日 · 1252 次阅读
本帖已被设为精华帖!

近日,Go 泛型语法负责人之一的Ian Lance Taylor 发布了一个 issue,说明 go 团队想引入新的 type set 概念,并去除原 Go 泛型方案中置于 interface 定义中的 type list 中的 type 关键字。

对于 Go 泛型来龙去脉不是很了解的童鞋,可以先去看看我看看我之前的文章:《能力越大,责任越大” - Go 语言之父详解将于 Go 1.18 发布的 Go 泛型》。在那篇文章的结尾,Go 设计团队对自己的 Go 泛型设计方案中的几个方面给出了自己的满意度评价,其中唯一让团队感觉还不是很完美的就是 “Type lists in interfaces”:

1. 何为 Type lists in interfaces

我们先来说说何为 Type lists in interfaces!当前 Go 泛型方案使用 interface 类型用于表达对类型参数 (type parameters) 的约束 (constraints),比如:

type MyC1 interface {
    M1()
}

func F1[T MyC1](t T) {

}

在上述代码中,我们使用 interface MyC1 作为类型参数 (type parameters) 的约束,对于 F1 函数而言,所有满足 MyC1 接口的类型都可以作为其类型参数的实参传入:

type MyT1 string
func(t1 *MyT1) M1() {}

var t1 = new(MyT1)
F1(t1)

*MyT1 实现了 MyC1 接口,于是我们可以将其实例 (t1) 传给 F1。Go 泛型的自动类型推导会将 T 的实参置为 *MyT1。

完整程序如下:

// https://go2goplay.golang.org/p/WPCvmwkxcEL
package main

import (
    "fmt"
)

type MyC1 interface {
    M1()
}

func F1[T MyC1](t T) {
    fmt.Printf("%T\n", t)
}

type MyT1 string

func (t1 *MyT1) M1() {

}

func main() {
    var t1 = new(MyT1)
    F1(t1) // *main.MyT1
}

对于自定义类型,通过实现接口的方法集合即可满足接口,对于类型参数可以是原生类型的情况,我们无法通过这种方式实现,于是 Go 团队将 type list 加入到 interface 接口中,仅用作泛型类型参数的约束检查

type MyC2 interface {
    type int, int32, int64
}

func F2[T MyC2](t T) {
    fmt.Printf("%T\n", t)
}

func main() {
    var t2 string
    F2(t2) // string
}

而 MyMC2 中的:

type int, int32, int64

就是所谓的"type list"。

如果一个 interface 定义中既有 method 也有 type list,那么要满足这个 interface 类型,则作为类型参数实参的类型既必须在 type list 中(或其 underlying type 在 type list 中),又必须实现接口类型的所有方法:

// https://go2goplay.golang.org/p/rE8mGH0lHWm
package main

import (
    "fmt"
)

type MyC3 interface {
    M3()
    type int, string, float64
}


func F3[T MyC3](t T) {
    fmt.Printf("%T\n", t)
}

type MyT3 string

func (t3 MyT3) M3() {

}

func main() {
    t3 := MyT3("hello")
    F3(t3) // main.MyT3
}

细心的童鞋会发现:拥有 type list 的 interface 仅能用于做为类型参数的约束,而不能像普通 interface 类型那样使用:

// https://go2goplay.golang.org/p/mJoEYrceBSL
package main

type MyC3 interface {
    M3()
    type int, string, float64
}

func main() {
    var i3 MyC3 // type checking failed for main 
                    // prog.go2:9:9: interface contains type constraints (int, string, float64)
    _ = i3
}

这种 gap(缝隙) 始终让 Go 核心团队的开发人员感到 “不爽”,那么能否将两者融合在一起呢?即放开对包含 type list 的 interface 类型仅能做 constraint 的限制,让其和普通 interface 一样使用。这次引入的 type set 应该是解决这个问题的一个前提。但在这个新 proposal 中,核心团队还没有将这个问题作为重点,只能算作是为以后留个作业吧。

2. 引入 type set 概念

Ian Lance Taylor 发布的这个 issue主要就是想引入 type set 概念,并用新语法等价替代原泛型 proposal中的 type list,新语法去除了原 type list 中的 type 关键字

于是 go 团队试图这样来做:


// 当前的type list
type SignedInteger interface {
    type int, int8, int16, int32, int64
}


// type set理念下的新语法
type SignedInteger interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

我们看到新语法中去掉了原先 type list 中的 type 关键字,类型间的间隔也由逗号改为了管道符 |。按该 proposal 的原意,管道符 (在布尔代数中也表示或) 更接近于 type list 的原意,即可以是 int,或 int8 或....。如果仅仅是变成了如下改进的语法:

type SignedInteger interface {
    int | int8 | int16 | int32 | int64
}

估计大家也没多大意见。但是偏偏引入了 “~” 这个前缀。~int 与 int 有什么区别呢?要搞清楚区别就要先来看看 Ian 新引入的 type set 概念了。

什么是 type set(类型集合)?Ian 给出了此概念的定义:

  • 每个类型都有一个 type set。
  • 非接口类型的类型的 type set 中仅包含其自身。比如非接口类型 T,它的 type set 中唯一的元素就是它自身:{T};
  • 对于一个普通的、没有 type list 的普通接口类型来说,它的 type set 是一个无限集合。所有实现了该接口类型所有方法的类型都是该集合的一个元素,另外由于该接口类型本身也声明了其所有方法,因此接口类型自身也是其 Type set 的一员。
  • 空接口类型 interface{}的 type set 中则是囊括了所有可能的类型;
  • 这样一来我们来试试用 type set 概念重新陈述一下一个类型 T 实现一个接口类型 I:即当类型 T 是接口类型 I 的 type set 的一员时,T 便实现了接口 I;
  • 对于使用嵌入接口类型组合而成的接口类型,其 type set 就是其所有的嵌入的接口类型的 type set 的交集。proposal 中的举例:type O2 interface{ E1; E2 } ,则 02 这个接口类型的 type set 是 E1 和 E2 两个接口类型的 type set 的交集。
  • 一个拥有一个 method 的接口类型,比如:
type MyInterface1 interface {
    MyMethod()
}

可以看成嵌入一个仅包含 MyMethod 的接口类型的接口类型:

type MyInterface interface {
    MyMethod()
}
type MyInterface1 interface {
    MyInterface
}
  • 因此,一个带有自身 Method 的嵌入其他接口类型的接口类型,比如:
type 03 interface {
    E1
    E2
    MyMethod03()
}

它的 type set 可以看成 E1、E2 和 E3(type E3 interface { MyMethod03}) 的 type set 的交集。

3. 替换 type list 的新语法方案

我们再回到前面提到的新语法方案:

// type set 新语法
type SignedInteger interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

Go 开发团队给那些用于作为约束或被嵌入到作为约束的接口类型中的接口类型的定义做了重新描述,称这类接口类型的定义中可以嵌入一些额外的结构,被称为interface elements,其组成如下图:

  • 图中 MyInterface 是一个仅用于约束或嵌入到作为约束的接口类型中的类型;
  • MyInterface 除了拥有自己的方法列表 (M1、M2) 外,还可以嵌入额外的结构:interface elements,就是 T1|T2|~T3|T4...|Tn 那一行,这一行即替代了原先方案中的 type list;
  • interface elements 这一行有三个值得关注的事情:
    • T1、T2、T4、Tn 这些仅代表 type set 仅为自身的类型;
    • ~T3 的 type set 为所有 underlying type 为 T3 的类型,~T3 被称为 approximation elements;
    • 管道符将这些类型连接在一起,共同构成一个 union element,该 union element 的 type set 为所有这些类型的 type set 的并集。

好了现在一切都建立在 type set 这个概念上。那么当上述接口类型作为类型参数的约束时,要想满足该约束,可以作为类型参数的实参,那么传入的类型应该在作为约束的接口类型的 type set 中。

有了前面关于 type set 以及接口嵌入的 type set 的铺垫,作为约束的接口类型的理解就容易多了。无论是单纯的接口类型还是使用嵌入其他接口组合而成的接口类型,亦或是既包括嵌入也拥有自己的 method list 的接口类型。

4. 问题

Ian 的 issue 一发出就得到了社区的重点关注,并引来的激烈的讨论,但从头看到尾,似乎大家都有些 “跑题”,关于这个 proposal 的真正疑问在于 approximation elements 身上:

  • 是否有必要单独拿出 approximation elements 这个概念

我们回顾一下当前泛型语法作为约束的接口定义所使用的 type list 语法,看看当前的 type list 语法中各个类型是否是仅代表自身?

// https://go2goplay.golang.org/p/5VbaSCQ8-Dq
package main

import (
    "fmt"
)

type S1 struct {
    Name string
    Age  int
}

type S2 S1

type MyC4 interface {
    type struct {
        Name string
        Age  int
    }, int
}

func F4[T MyC4](t T) {
    fmt.Printf("%T\n", t)
}

type MyInt int

func main() {
    var t1 = S1{"tony", 17}
    F4(t1) // main.S1
    var t2 = S2{"tony", 17}
    F4(t2) // main.S2
    var n MyInt = 3
    F4(n) // main.MyInt
}

我们看到作为约束的接口类型 MyC4 的 type list 中有两个类型:一个匿名 struct 和 int。之后我们分别使用 S1、S2 和 MyInt 作为类型参数的实参,居然都通过了!也就是说当前的 type list 中的类型按照 type set 的概念解释,都属于 approximation element,只要是 underlying type 在 type list 中,那么就可以作为类型参数的实参,通过约束检查。

那就是说:

我们是否可以只将:

type I1 interface {
    type int, string, float64
    ... ...
}

换成:

type I1 interface {
    int | string | float64
    ... ...
}

而无需~这个符号呢?

  • 如果~符号是必要的,可否不用~符号?

Go 语言中没有使用~运算符,但这个符号在其他主流语言,比如 C 中是位运算符,而且代表的 “非” 这个运算符。因此将其用在类型 T 前面,打眼一看,以为其含义是 “不是类型 T 的类型”。而新 proposal 则将其用于表示 approximation element。这让很多 gopher 提出异议,希望换一个符号,比如 T+ 等。但目前尚无定论。

5. 小结

能力有限,以上一些对该 proposal 的理解可能有误,欢迎交流指正。

type set 并没有改变什么,只是完成了对 interface 与实现 interface 的重新解释。 但是对于后续将 interface element 用于普通 interface 类型定义可能有重大的意义。当前的带有 interface element 的 interface 类型仅能用于作为泛型类型参数的约束,这与普通 interface 之间的 gap 早晚要 “填上”,不过这已经不是这个 proposal 要解决的事情。

从泛型提出到如今,我已经感到泛型的引入极大增加了复杂性 ,即便没有滥用泛型,没有耍奇技淫巧,泛型的引入也让 go 复杂性陡增。就像这个 proposal,认真阅读并理解还是需要花费不少时间和精力的。


Go 技术专栏 “改善 Go 语⾔编程质量的 50 个有效实践” 正在慕课网火热热销中!本专栏主要满足>广大 gopher 关于 Go 语言进阶的需求,围绕如何写出地道且高质量 Go 代码给出 50 条有效实践建议,上线后收到一致好评!欢迎大家订 阅!

img{512x368}

我的网课 “Kubernetes 实战:高可用集群搭建、配置、运维与应用” 在慕课网热卖>中,欢迎小伙伴们订阅学习!

img{512x368}

Gopher Daily(Gopher 每日新闻) 归档仓库 - https://github.com/bigwhite/gopherdaily

我的联系方式:

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
astaxie 将本帖设为了精华贴 04月09日 03:31
_Lin GoCN 每日新闻 (2021-04-09) 中提及了此贴 04月09日 03:32
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册