Zoe

Zoe

beta
实验项目

返回到博客

试试 Golang 的泛型

Zoe

Zoe

2022-03-17

1 MINS

最近Golang发布了1.18版本,其中一个很大的新功能就是泛型(generics)的支持, 这个也算是社区中呼唤的比较久的功能了。官方称早在2010年就在尝试支持吃, Proposal应该是在2021年1月正式提出:Type Parameters Proposal

有时候在用了一段Rust或TypeScript后,再回来写Golang代码时,对于泛型的需求会更加强烈。

何为泛型?#

简单来说,定义(函数、结构体等)时用类型占位,在使用时确定具体类型。 泛型能让代码的复用性提高很多,而且对于有利于逻辑的抽象。

尝试一用#

假设我们现在需要实现一个函数,用来返回数组中的最大值,其中数组的值类型可能是所有能够比较大小的类型。 没有泛型的支持下,在Golang中可能是以下的几种函数签名,

  1. 分别定义函数
1func maxIntFrom(items []int) int {}
2func maxCharFrom(items []char) char {}
3// ...
4// 如果需要支持其他值类型,则需要分别定义
  1. 使用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 = item
7 }
8 }
9
10 one
11}

使用时,如下这样,

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 = item
6 }
7 }
8 return one
9}
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 | byte
3}
4
5func largest[T Comparable](items []T) T {}

再来看看,已经和Rust很像啦。只不过,Comparable只是一个类型的集合而已。

能不能再进一步,做到和Rust一毛一样,用Trait(类型接口)来做类型约束?当然是可以的, 不然也太鸡肋了。

1type Stringer interface {
2 String() string
3}
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个数组值类型可以是不同类型的,因为泛型不同T1T2, 而Print2则需要2个数组值类型则必须为统一类型,因为泛型都是泛型T

所以,类型是看泛型类型,而不是约束类型。这也是泛型的一大用处,用于约束不同位置的类型。

我有一个工具库函数是效仿Rust的迭代器(Iter)的,之前的由于没有泛型, 只能通过interface{}来支持不同的值类型,所以先断言成[]interface{}才能继续后面的操作。具体的代码是实现如下,

1type Iter struct {
2 items []interface{}
3
4 // 实际实现均为 []
5 filters func(interface{}) bool
6 maps func(interface{}) interface{}
7
8 err error
9}
10
11func (i *Iter) Filter(f func(interface{}) bool) *Iter {
12 i.filters = f
13 return i
14}
15
16func (i *Iter) Map(f func(interface{}) interface{}) *Iter {
17 i.maps = f
18 return i
19}
20
21func (i *Iter) Collect() []interface{} {
22 // cache
23 res := []interface{}{}
24 for _, item := range i.items {
25 if i.filters != nil && !i.filters(item) {
26 continue
27 }
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 res
36}
37
38func (i *Iter) Error() error {
39 return i.err
40}
41
42func NewIter(items interface{}) *Iter {
43 iter := &Iter{}
44
45 // ignore is not a slice or array
46 v := reflect.ValueOf(items)
47 if v.Kind() != reflect.Slice {
48 iter.err = errors.New("Iteror value not supported")
49 return iter
50 }
51 for i := 0; i < v.Len(); i++ {
52 iter.items = append(iter.items, v.Index(i).Interface())
53 }
54
55 return iter
56}

那么,在泛型支持下,我们应该如何实现呢

1type Iter[T any] struct {
2 items []T
3 cursor int
4 size int
5}
6
7// TODO

由于不能在方法上使用泛型,所以不能很好的实现。

我们再看个例子,还是我之前写的一个工具库中的例子, 由于Golang没有三元表达式或者Default,所以只能写if判断。 所以我写了一个判断链的用法,

1type Value struct {
2 val interface{}
3 cond bool
4}
5
6// NewValue create the value for expression
7func 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.val
19}
20
21// Or Value(a).Or(-1)
22func (v *Value) Or(r interface{}) *Value {
23 // check if is else
24 // check if val is nil or zero value
25 if !v.cond || v.val == nil || reflect.ValueOf(v.val).IsZero() {
26 v.val = r
27 }
28 return v
29}
30
31// If Value(a).If(a == 1).Or(0)
32// can accept a func() bool
33func (v *Value) If(r bool) *Value {
34 v.cond = r
35 return v
36}

可以看出来,很不好用,需要用reflect,还需要手动取值类型。 那么有了泛型后实现会不会精简很多呢,我们直接看效果。

1type Value[T comparable] struct {
2 val T
3
4 cond bool
5 zero T
6}
7
8// NewValue create the value for expression
9func 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.val
18}
19
20// Or Value(a).Or(-1)
21func (v *Value[T]) Or(r T) *Value[T] {
22 // check if is else
23 // check if val is nil or zero value
24 if !v.cond || v.zero == v.val {
25 v.val = r
26 }
27 return v
28}
29
30// If Value(a).If(a == 1).Or(0)
31// can accept a func() bool
32func (v *Value[T]) If(r bool) *Value[T] {
33 v.cond = r
34 return v
35}

看起来精简太多了。而且不用reflect调用,也不用各种取值方法。 顺便测试了一下性能,比用iterface{}的方式好约6倍,当然还是不及直接写快。 不过这并不是泛型带来的问题,而是这种封转必然会带来性能损耗。

1goos: darwin
2goarch: amd64
3pkg: demo.zoe.im/go-generic
4cpu: Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz
5BenchmarkXValue_Normal-8 1000000000 0.3282 ns/op 0 B/op 0 allocs/op
6BenchmarkXValue_InterfaceCall-8 24450914 48.52 ns/op 0 B/op 0 allocs/op
7BenchmarkXValue_GenericCall-8 144149385 8.310 ns/op 0 B/op 0 allocs/op
8PASS
9ok demo.zoe.im/go-generic 3.754s

总结#

Golang的泛型是千呼万唤总出来,在1.18中正式发布。 如果有一些编程经验的话,会有比较明确的需求,特别是有其他语言的泛型经验。 和以前相比修改点主要是在函数定义和类型定义后。

  • 分割符号是[],不同于Rust的<>
  • 位置位于函数名或类型名之后

泛型写法和类型一致,

  • 泛型名和约束之间用空格( ): T: int | string
  • 多个之间用,进行分割: T1 int | int64, T2 string
  • 泛型约束可以合并: T1, T2 any注意这不代表T1T2是同一个泛型

类型约束有一下3种,

  • 声明类型,int | byte
  • 接口,Stringer
  • 任意类型,interface{}

当然了,当前Go的泛型还是有诸多的限制,想要了解具体的设计细节可以看看 Type Parameters Proposal推荐看看,里面很多使用的细节。


高龄野生码农的云原生面试
B端项目建设分享

Zoe

Zoe

beta

我将成为我的墓志铭

站点

© 2011 - 2022 Zoe - All rights reserved.

Made with by in Hangzhou.