发布于 

Go1.17探索与泛型尝鲜

Go 团队每年发布两次大版本,一般是在二月份和八月份发布。今天中午,Go1.17也是如期而至,带来各种优化和新功能,但是似乎泛型还没支持,但泛型的实现和测试已经包含在代码中,只是没有默认开启对泛型的支持,这也是为 Go1.18 版本泛型正式实装做铺垫。这样意味着最早在 2022年2月,我们就可以正式使用泛型进行代码开发了。

2021年8月21日,Go泛型在gotip中已经默认启用

泛型是什么, 有了它对我们有什么影响

在没有泛型时,如果想要实现类型转换,可以如何实现?那代码结构类似如下:

interface{}

比如说使用interface{}作为变量类型参数,在内部通过类型判断进入对应的处理逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
...
switch vt := val.Interface().(type) {
case fmt.Stringer:
return vt.String()
case int:
return strconv.Itoa(vt)
case int8:
return strconv.Itoa(int(vt))
case int16:
return strconv.Itoa(int(vt))
case int32:
return strconv.Itoa(int(vt))
case int64:
return strconv.FormatInt(vt, 10)
case string:
return vt
case uint:
return strconv.FormatUint(uint64(vt), 10)
case uint8:
return strconv.FormatUint(uint64(vt), 10)
case uint16:
return strconv.FormatUint(uint64(vt), 10)
case uint32:
return strconv.FormatUint(uint64(vt), 10)
case uint64:
return strconv.FormatUint(vt, 10)
case []byte:
return string(vt)
default:
...

Duck typing

将类型转化为特定表现的鸭子类型,通过接口定义的方法实现逻辑整合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
...
type Ducker interface {
quack()
feathers()
}

type Duck struct{}
type Person struct{}

func (this *Duck) quack() {
fmt.Println("Duck croak")
}

func (this *Duck) feathers() {
fmt.Println("Ducks have gray and white feathers")
}

func (this *Person) quack() {
fmt.Println("Someone imitates the duck quack")
}

func (this *Person) feathers() {
fmt.Println("Someone showing white and gray feathers")
}

func in_the_forest(d Ducker) {
d.quack()
d.feathers()
}

func main() {
donald := &Duck{}
john := &Person{}
in_the_forest(donald)
in_the_forest(john)
}
...

引入泛型的好处

  • 通过泛型,可以针对多种类型编写一次代码,大大节省了编码时间。充分利用编译器的编译检查,保证程序的可靠性和鲁棒性。
  • 借助泛型,可以减少代码的重复度,避免一处出现问题需要修改多处地方的尴尬情况。

Go 泛型发展

Go 官方团队泛型提案文档 Type Parameters in GoProposal: Go should have generics以及相关 issue

Go 泛型尝鲜

我们尝试对上文中的类型转换使用泛型的方式重新编写一个小例子。

从简单的例子开始

Go 是强类型语言, 所以编写程序时传入变量需要指定 intstring 类型, 大致的函数如下:

1
2
func PrintString(s string) {}
func PrintInt(i int) {}

试想下,如果对于不同类型的参数,需要为每个变量类型编写一个函数去实现,当然,我们可以使用 interface{} 来实现,但这样有很多的局限性。

首先定义一个泛型函数 PrintAnything,它允许接收任何类型的参数(定义为 T),函数定义如下:

1
func PrintAnything[T any](thing T) {}

any 可以表示接收任意类型的入参,此时要实现上文中函数可以这样调用实现相同的效果:

1
2
PrintAnything("Hello! Go generics")
PrintAnything(1.17)

如果这时我们使用简单的go run命令运行,会发现提示语法错误:

1
2
3
4
5
6
7
❯❯ Downloads  12:26 go version
go version go1.17 windows/amd64
❯❯ Downloads 12:26 go run a.go
# command-line-arguments
.\main.go:4:2: syntax error: unexpected type, expecting method or interface name
.\main.go:9:6: missing function body
.\main.go:9:14: syntax error: unexpected [, expecting (
Tips:

Go1.17正式版本发布时,泛型代码已经内置,但默认未开启编译支持,我们可以通过支持泛型的在线Playground版本中调试,或者下载beta版本的go2go tool进行调试

在 Go1.17版本中,我们也可以通过gcflags方式进行尝鲜

1
2
3
$ go run -gcflags=-G=3 ~/main.go
Hello! Go generics
1.17

约束

从上面的例子中我们好像很容易的利用泛型实现了一个类似 fmt 包中打印变量的功能。接下来我们继续尝试实现 strings.Join,让它接受一个任意类型的切片,并返回连接之后的字符串。

1
2
3
4
5
6
func Join[T any](things []T) (result string) {
for _, v := range things {
result += v.String()
}
return result
}

如果这时我们使用简单的go run命令运行,会发现提示语法错误:

1
2
output := Join([]string{"a", "b", "c"})
// v.String undefined (type bound for T has no method String)

Join函数需要将任意类型转换为string类型,需要检查v是否有String()方法,我们需要将代码改造下,让T是一个约束类型,并实现String()方法:

1
2
3
type Stringer interface {
String() string
}

所以我们的Join函数现在的定义为:

1
func Join[T Stringer] ...

由于Stringer保证 T 类型的任何值都有一个String()方法。但是,如果你尝试调用Join()某些类型不能满足Stringer(例如int):

1
2
result := Join([]int{1, 2, 3})
// int does not satisfy Stringer (missing method String)

比较运算符约束

例如我们需要实现一个比较两个参数是否相等的函数Equal,它接受两个T类型的参数,相等返回true否则返回false,代码如下:

1
2
3
4
5
6
func Equal[T any](a, b T) bool {
return a == b
}

fmt.Println(Equal(1, 1))
// cannot compare a == b (operator == not defined for T)

这里和上文是同一种问题,因为我们在Join()String()的方法调用,但这里我们不能使用基于方法集的约束。我们需要将 T 限制为仅使用==!=运算符的类型,这些类型称为可比较类型。使用内置comparable类型约束,而不是any.

1
func Equal[T comparable] ...

constraint 包

接下来,我们继续利用泛型做一些事情,比如有一个切片,我们需要取出最高值的元素,代码可能如下:

1
2
3
4
5
6
7
8
func Max[T any](input []T) (max T) {
for _, v := range input {
if v > max {
max = v
}
}
return max
}

尝试运行,不出意外是肯定报错了

1
2
fmt.Println(Max([]int{1, 2, 3}))
// cannot compare v > max (operator > not defined for T)

从报错来看,T 类型必须是可以有序可排列的。

1
2
3
4
5
6
type Ordered interface {
type int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64,
string
}

或者我们可以使用内置的constraint

1
func Max[T constraint.Ordered] ...

泛型类型

我们已经利用类型实现了可以接受任何类型参数的函数。但是,如果我们想要建立一个类型,可以包含任何类型的?例如,

  • “任何类型的切片”类型:
    1
    type Bunch[T any] []T
    对于任何给定的类型 T,Bunch[T]是值为T的切片。例如,
  • Bunch[int]是值为int的一个切片
    1
    x := Bunch[int]{1, 2, 3}
  • 采用泛型类型的泛型函数
    1
    func PrintBunch[T any](b Bunch[T]) {}
  • 为泛型添加方法
    1
    func (b Bunch[T]) Print() {}
  • 对泛型类型应用约束
    1
    type StringableBunch[T Stringer] []T