返回到博客
试试 Golang 的泛型
最近Golang发布了1.18
版本,其中一个很大的新功能就是泛型(generics)
的支持,
这个也算是社区中呼唤的比较久的功能了。官方称早在2010年就在尝试支持吃,
Proposal应该是在2021年1月正式提出:Type Parameters Proposal。
有时候在用了一段Rust或TypeScript后,再回来写Golang代码时,对于泛型的需求会更加强烈。
何为泛型?#
简单来说,定义(函数、结构体等)时用类型占位,在使用时确定具体类型。 泛型能让代码的复用性提高很多,而且对于有利于逻辑的抽象。
尝试一用#
假设我们现在需要实现一个函数,用来返回数组中的最大值,其中数组的值类型可能是所有能够比较大小的类型。 没有泛型的支持下,在Golang中可能是以下的几种函数签名,
- 分别定义函数
1func maxIntFrom(items []int) int {}2func maxCharFrom(items []char) char {}3// ...4// 如果需要支持其他值类型,则需要分别定义
- 使用
interface{}
1func maxFrom(items []interface{}) interface{} {}
可以看出,方法1是没办法统一调用函数的,而且不能复用判断逻辑。
方法2虽然看上去是统一了调用函数,但是其本质还是方法1。
而且引入了interface{}
,增加参数转换和运行时的类型判断。
另外,[]interface{}
并不是严格预期,我们需要接收的参数是,
所有能比较大小类型的数组。当然了,由于Golang中Trait
这类概念,
所以这一点是必然没办法达做到的。
在尝试用Golang的泛型之前,先看一下在Rust中是如何解决,
1fn largest<T: PartialOrd>(items: &[T]) -> T {
其中T
作为类型的占位符,在函数名后表明,然后参数中的数组的值类型是T
,
函数返回值类型也是T
,如此就可以了。至于T: PartialOrd
是Rust中的对于
T
的限定的表示,PartialOrd
是能够进行大小比较的Trait。
完整的Rust函数代码参考下面,
1fn largest<T: PartialOrd + Copy>(items: &[T]) -> T {2 let mut one = items[0];3
4 for &item in items {5 if item > one {6 one = item7 }8 }9
10 one11}
使用时,如下这样,
1fn main() {2 println!("{}", largest::<i32>(&[1,2,3]));3 println!("{}", largest(&['a', 'b', 'c']));4}
function_name::<具体类型>
,在这里编译时,会把具体类型去做使用。
当然了,由于Rust可以自主推导类型,调用时也可以不用指明。
真正用于比较的逻辑,只需要实现一遍就可了。
对比着看Rust,我们回到Golang中来。我们发现一个问题,Trait
在Golang可以理解为interface{}
接口类型,
那么如何来对T
进行限定呢?
其实也很简单,我们回到问题的本质是支持多种值类型的数组
,只需要用多个类型来限定T
就可以了。
按照上面的思路函数签名应该是,
1func largest[T int | byte](items []T) T {}
完整的代码实现如下,
1func largest[T int | byte](items []T) T {2 one := items[0]3 for _, item := range items {4 if item > one {5 one = item6 }7 }8 return one9}10
11func main() {12 println(largest[int]([]int{1,2,3}))13 println(largest([]byte{'a','b','c'}))14}
可以看到Golang中使用时也是可以省略T
的具体类型表示,由编译器自行推导处理。
🎉 看起来还不错。
这就Go(够)了吗?#
只能每次都的类型约束都只能在一个个写吗?
这一点,Golang已经考虑到了,可以让我们定义类型约束,当然还是通过万能的interface{}
,
1type Comparable interface {2 int | byte3}4
5func largest[T Comparable](items []T) T {}
再来看看,已经和Rust很像啦。只不过,Comparable
只是一个类型的集合而已。
能不能再进一步,做到和Rust一毛一样,用Trait
(类型接口)来做类型约束?当然是可以的,
不然也太鸡肋了。
1type Stringer interface {2 String() string3}4
5func echo[T Stringer](s T) {}
这个和之前的接口类型参数用法有什么区别?
1func echo(s Stringer) {}
我们可以这样来理解,之前需要自己去定义Stringer
,然后让其他结构体来实现,
才能够满足函数的使用。对于其他外部的依赖,就不能传进来了。
那么现在这个可以说是,一个方法的集合,不管是外部的结构体(接口)还是自己实现的, 只要包含约束类型的方法就可了。
这样一来,就基本差不多了。
再看一看#
现在回过头来结合Type Parameters Proposal看类型约束,
- 声明类型,
int | byte
- 接口,
Stringer
- 任意类型,
interface{}
我们再来看一个问题,
1func Print1[T1, T2 any](s1 []T1, s2 []T2) {}2func Print2[T any](s1 []T, s2 []T) { ... }
Print1
参数的2个数组值类型可以是不同类型的,因为泛型不同T1
和T2
,
而Print2
则需要2个数组值类型则必须为统一类型,因为泛型都是泛型T
所以,类型是看泛型类型,而不是约束类型。这也是泛型的一大用处,用于约束不同位置的类型。
我有一个工具库函数是效仿Rust的迭代器(Iter
)的,之前的由于没有泛型,
只能通过interface{}
来支持不同的值类型,所以先断言成[]interface{}
才能继续后面的操作。具体的代码是实现如下,
1type Iter struct {2 items []interface{}3
4 // 实际实现均为 []5 filters func(interface{}) bool6 maps func(interface{}) interface{}7
8 err error9}10
11func (i *Iter) Filter(f func(interface{}) bool) *Iter {12 i.filters = f13 return i14}15
16func (i *Iter) Map(f func(interface{}) interface{}) *Iter {17 i.maps = f18 return i19}20
21func (i *Iter) Collect() []interface{} {22 // cache23 res := []interface{}{}24 for _, item := range i.items {25 if i.filters != nil && !i.filters(item) {26 continue27 }28 if i.maps != nil {29 v := i.maps(item)30 res = append(res, v)31 } else {32 res = append(res, item)33 }34 }35 return res36}37
38func (i *Iter) Error() error {39 return i.err40}41
42func NewIter(items interface{}) *Iter {43 iter := &Iter{}44
45 // ignore is not a slice or array46 v := reflect.ValueOf(items)47 if v.Kind() != reflect.Slice {48 iter.err = errors.New("Iteror value not supported")49 return iter50 }51 for i := 0; i < v.Len(); i++ {52 iter.items = append(iter.items, v.Index(i).Interface())53 }54
55 return iter56}
那么,在泛型支持下,我们应该如何实现呢
1type Iter[T any] struct {2 items []T3 cursor int4 size int5}6
7// TODO
由于不能在方法上使用泛型,所以不能很好的实现。
我们再看个例子,还是我之前写的一个工具库中的例子,
由于Golang没有三元表达式或者Default
,所以只能写if
判断。
所以我写了一个判断链的用法,
1type Value struct {2 val interface{}3 cond bool4}5
6// NewValue create the value for expression7func NewValue(v interface{}) *Value {8 return &Value{9 val: v,10 cond: true,11 }12}13
14// 省略各种类型的取值方法15
16// Interface ...17func (v *Value) Interface() interface{} {18 return v.val19}20
21// Or Value(a).Or(-1)22func (v *Value) Or(r interface{}) *Value {23 // check if is else24 // check if val is nil or zero value25 if !v.cond || v.val == nil || reflect.ValueOf(v.val).IsZero() {26 v.val = r27 }28 return v29}30
31// If Value(a).If(a == 1).Or(0)32// can accept a func() bool33func (v *Value) If(r bool) *Value {34 v.cond = r35 return v36}
可以看出来,很不好用,需要用reflect
,还需要手动取值类型。
那么有了泛型后实现会不会精简很多呢,我们直接看效果。
1type Value[T comparable] struct {2 val T3
4 cond bool5 zero T6}7
8// NewValue create the value for expression9func NewValue[T comparable](v T) *Value[T] {10 return &Value[T]{11 val: v,12 cond: true,13 }14}15
16func (v *Value[T]) Value() T {17 return v.val18}19
20// Or Value(a).Or(-1)21func (v *Value[T]) Or(r T) *Value[T] {22 // check if is else23 // check if val is nil or zero value24 if !v.cond || v.zero == v.val {25 v.val = r26 }27 return v28}29
30// If Value(a).If(a == 1).Or(0)31// can accept a func() bool32func (v *Value[T]) If(r bool) *Value[T] {33 v.cond = r34 return v35}
看起来精简太多了。而且不用reflect
调用,也不用各种取值方法。
顺便测试了一下性能,比用iterface{}
的方式好约6倍,当然还是不及直接写快。
不过这并不是泛型带来的问题,而是这种封转必然会带来性能损耗。
1goos: darwin2goarch: amd643pkg: demo.zoe.im/go-generic4cpu: Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz5BenchmarkXValue_Normal-8 1000000000 0.3282 ns/op 0 B/op 0 allocs/op6BenchmarkXValue_InterfaceCall-8 24450914 48.52 ns/op 0 B/op 0 allocs/op7BenchmarkXValue_GenericCall-8 144149385 8.310 ns/op 0 B/op 0 allocs/op8PASS9ok demo.zoe.im/go-generic 3.754s
总结#
Golang的泛型是千呼万唤总出来,在1.18
中正式发布。
如果有一些编程经验的话,会有比较明确的需求,特别是有其他语言的泛型经验。
和以前相比修改点主要是在函数定义和类型定义后。
- 分割符号是
[]
,不同于Rust的<>
- 位置位于函数名或类型名之后
泛型写法和类型一致,
- 泛型名和约束之间用空格(
T: int | string
- 多个之间用
,
进行分割:T1 int | int64, T2 string
- 泛型约束可以合并:
T1, T2 any
,注意这不代表T1
和T2
是同一个泛型
类型约束有一下3种,
- 声明类型,
int | byte
- 接口,
Stringer
- 任意类型,
interface{}
当然了,当前Go的泛型还是有诸多的限制,想要了解具体的设计细节可以看看 Type Parameters Proposal。推荐看看,里面很多使用的细节。