go1.18泛型支持
Posted 2022-11-11 01:00 +0800 by ZhangJie ‐ 2 min read
go1.18 泛型支持
关于泛型编程
首先什么是泛型呢?
Generic programming is a style of computer programming in which algorithms are written in terms of types to-be-specified-later that are then instantiated when needed for specific types provided as parameters.
泛型编程有啥好处呢?
- cleaner code and simpler API (not always)
- improve code exectution performance (not always)
没有泛型的日子
如何应付的
go1.18之前苦于没有范型编程,开发人员一般会这么做:
- go编译器对内置类型有一定的范型支持,比如new、make、len、cap
- go支持reflection和interace,通过这两个一定程度上可以模拟范型的能力
- go支持//go:generate,通过自定义工具可以生成一些“重复”代码
痛点依然在
即便是通过反射、interface来模拟也把风险从编译时类型安全推到了运行时检查部分,生成代码也会有大量重复性代码……所以痛点依然存在。
go1.18中终于解决了这个问题,虽然现在来看还没那么尽善尽美,但是总算在路上了。
go泛型知识点
go1.19当前范型设计实现,也还没完全实现提案type parameters proposal,这个提案也并非未来go泛型实现的天花板,会一步步完善。尽管还不尽善尽美,但是将来go泛型编程应该有较大的应用场景。现在有些库已经在用泛型重写了。
自定义泛型:
1.18支持了自定义范型(customized generics),这个提法是为了与内置的泛型支持区分开。所说的内置泛型,指的是类似new、make、len、cap这样的一些函数,或者map[k]v这样的数据结构类型,这些有泛型的思想和支持。
但是我们所说的泛型主要是指自定义的泛型类型、函数、方法。
基础知识:
- 泛型类型 type Lockable[T any]
- 泛型方法 func(l *Lockable[T]) Do(f func(*T)) {…}
- 泛型函数 func Equal[T comprable](a, b T) bool { return a == b}
- 如果类型参数列表中有多个,如[a any, b, c, _ comparable],它们的顺序没有影响的
接口表示:
- tilde form:~T,波浪号+类型,表示类型集合,表示所有underlying type为T的类型
- term form:T1 | T2 | …. | Tn,类型的联合
接口嵌套:
1.18之前接口内可以嵌入任意数量函数、任意类型名(只要类型名为接口名即可)
1.18中放松了嵌入类型名的限制,可以是
- 任意类型的字面量,只要不是类型参数名即可,比如string、其他接口名
- 无名接口定义
- tilde形式
- term形式
而类型参数的constraint其实就是interaface,这里的增强大大增强了constraint描述的范畴,如所有的int、uint、float,或者string
下面是些合法的接口定义
type L interface { Run() error Stop() } type M interface { L Step() error } type N interface { M interface{ Resume() } ~map[int]bool ~[]byte | string } type O interface { Pause() N string int64 | ~chan int | any }
在一个接口A中嵌入另一个接口B,相当于把B递归的展开把方法全部作为A的方法,比如接口0相当于这样,其中的不是方法名的部分,~map[int]bool…int64|~chan int|any,可以看做不同的term union form。
注意interface { int; uint } 表示底层类型同时是int和uint的,而interface{int | uint}表示底层类型或者是int或或uint的,是两种完全不同的概念。
type O interface { Run() error Stop() Step() error Pause() Resume() ~map[int]bool ~[]byte | string string int64 | ~chan int | any }
如果constraint中只包含一个元素,而且它是type element,那么可以省略外层的interface{},比如[T interface{~int}]可以简化为[T ~int]
但是上述简化,有时也会遇到解析问题,比如[T *int],这里表示的是啥意思呢?是underlying type为 int的范型类型?还是把int当做一个常量解释为一个Tint这么大的数组?现在确实是当做数组的。编译器当然可以解决这个问题,但是得做些额外的处理,后面可能会优化吧。
weired,那现在如何化解这个问题?
- 可以用完整形式,用interface裹起来
- 在最后加一个逗号结尾[T *int,],类型参数列表最后允许加逗号的,换行的话也要用逗号连接
我擦,就不要用这种破坏可读性的方式来写,直接用interface{}包起来!
在看个奇葩的[A int, B *A],我擦这里的A到底是啥?I don’t know!
- 尽管类型参数的constraint是一个接口,但是不代表类型参数就可以像普通接口变量那样可以有动态值、可以断言,我们把它理解成一个类型、把constraint理解成一种限制就可以了,不要总想着它是一个接口值的变量(实际上也不是)。
类型参数作用域:
- 参考这里:http://localhost:55556/generics/555-type-constraints-and-parameters.html#:~:text=Go specification says,of the type.
- 举个例子:
type G[S ~[]E, E int] struct{}
,这里的E后面有作为了S的声明,对于函数、方法也是类似的。就是说一个类型参数(比如E)的作用域从这个类型、函数、方法定义开始就有效,直到定义结束。所以这里的E是有效的,对于S它自然要找E在哪定义的,怎么找,在当前scope里面找,因为specification这么定义的,它当然在这个scope里找定义了。跟从左往右、从右往左这种表面上的顺序无关。
类型参数实例化、类型推导:
- 其实包括泛型类型中的类型参数实例化,和泛型函数、泛型方法中的类型参数实例化
实例化时参数列表省略问题:
- 泛型类型中:省略类型参数列表不能省略,要写完整的
- 泛型函数、泛型方法:当可以推断时,可以部分省略或完全省略
实例化时传递的实参问题:
- 基本接口类型any、error可以作为类型参数的实例化参数。
- 如果一个类型参数A的constraint满足另一个类型参数B的constraint,那么可以传递A的实例化作为B对应的类型参数的实例化参数
类型参数上的操作
看这里吧:http://localhost:55556/generics/777-operations-on-values-of-type-parameter-types.html,实在是费解,这么多特殊规则,谁能记得住怎么写
有些操作是有效的,有些则是无效的。通俗地说,某个操作是否有效,要看其对类型参数对constraint所表示的type set中每个类型是否都有效,都有效才算是有效的。
go自定义泛型不是通过c++模版那样重复生成代码实现的,这也是和代码生成的不同之处。有一条principle rule就是:在go里面,每个有类型的表达式计算都必须有一个指定的类型,这里的类型可以是普通类型,也可以是类型参数。这条原则很关键,比方说typeset包含多个候选类型参数值,函数体里对值的操作表达式对应的类型需要有一个核心类型来表示。比如下面这个:
// 如果T是chan int,那么从c读到的是int,如果T是chan bool,那么从c读初的是bool, // 一个是int,一个是bool,不能统一到同一个类型,这就叫core type missing, // 此时定义一个类型参数作个衔接就可以了。 func read[T chan int|chan bool](c T) { _ = <-c } // 改成这样就可以了 func read[T chan E, E int|bool](c T) { _ = <-c }
这里的限制多写写可能会更好理解,多看多学吧,理解go泛型还真有点费解,哈哈哈 🙂
go泛型技术细节
go1.18中的generics(范型)实现思路:
- stenciling(蜡印)这种方式会为范型函数的每种用到的类型参数实例化一个函数,这种好处是会减少函数调用开销,缺点是会导致binary尺寸过大,也可能会导致一些当前无法预见的问题。
- dictionary(字典)这种方式不会像stenciling那样为每个类型参数实例化一个,而是就生成一个函数实例,但是多给它传入一个dictionary指针参数,这个指针参数里包含了实际用到的类型参数(parameterized type)对应的具体类型参数(concrete type),有了这个信息就能知道实参的size、alignment等信息,这样在安排内存、栈帧、函数参数、返回值的时候就知道该如何组织了,后续的就没什么复杂的了。这种方式的一个问题,是需要考虑如何优化调用的性能开销。
- gcshape,它描述的是在内存allocator、garbage collector视角看起来描述信息长得类似的type,这种type相同的可以实例化一个,这样的话可能有多次实例化,就暗含了stenciling的思想,虽然还是多了传字典,但是省了因为底层type不一样时所要做的额外工作。
本文小结
本文简单总结了go泛型编程的一些知识点、注意事项,以及简要介绍了go泛型的设计实现原理。内容没有展开太多,感兴趣的可以参考文章末了列出的参考文献。
参考内容
泛型编程:https://en.wikipedia.org/wiki/Generic_programming
初识go泛型:http://localhost:55556/generics/444-first-look-of-custom-generics.html
类型参数限制:http://localhost:55556/generics/555-type-constraints-and-parameters.html
类型参数实例化:http://localhost:55556/generics/666-generic-instantiations-and-type-argument-inferences.html
对类型参数值的操作:http://localhost:55556/generics/777-operations-on-values-of-type-parameter-types.html
go泛型目前的状态:http://localhost:55556/generics/888-the-status-quo-of-go-custom-generics.html
go1.18 generics设计实现:https://github.com/golang/proposal/blob/master/design/generics-implementation-dictionaries-go1.18.md