Go 1.17 泛型!

作为 Go 最广为吐槽的两点之一,泛型的引入可以算是到目前为止最受人关注的更新(另一个广泛吐槽的是错误处理)。

开启泛型

在 Go 1.17 中已经存在了泛型的实现,但是并未默认开启,需要使用 -gcflags=-G=3 来主动开启

示例代码

package main

import "fmt"

type Number interface{
	type int, int8, int16, int32, int64, 
		uint, uint8, uint16, uint32, uint64,
		float32, float64
}

func max[T Number](a, b T) T {
	if a > b {
		return a
	}
	return b
}

func main() {
	fmt.Println(max(float64(1), float64(2)))
}

使用 go run -gcflags=-G=3 main.go 即可运行

$ go run -gcflags=-G=3 main.go
2

语法

泛型整体分为四部分

泛型定义

与其他语言的泛型不完全一致,Go 的泛型更类似 interface{},只是他会在编译器对代码进行处理
需要通过如下的形式,来声明泛型可以支持哪些参数,在后续使用过程中,需要保证所有对泛型的操作都满足这里对应的类型要求

type Number interface{
	type int, int8, int16, int32, int64, 
		uint, uint8, uint16, uint32, uint64,
		float32, float64
}

泛型声明

一个泛型定义后,需要在函数中声明如何去使用它

在这里,函数名后的 [T Number] 就是声明了一个类型 TT 满足 Number 的约束,后续的操作即可使用 T 来表示类型。

func max[T Number](a, b T) T {
	if a > b {
		return a
	}
	return b
}

如果不对泛型进行限制,可以使用 any,也即

func max[T any](a, b T) T {
	if a > b {
		return a
	}
	return b
}

其使用与 C++ 的模板类似,只是多了一个对于类型的约束(不过既然有编译器展开时类型检查的话,实际上约束应该更多是对代码规范的限制?)

template <class T>
void swap(T &a, T &b) {
    T temp;
    temp = a;
    a = b;
    b = temp;
}

泛型使用

当函数名后定义好类型后,在整个函数参数、函数返回值、函数体都可以使用类型 T(可以理解为一个存储类型的变量)
在这里可以对泛型执行任意的操作,比如如果泛型都是数字,那么可以进行加法或赋值一个数字,类型会被自动进行转换

泛型调用

最后就是调用一个支持泛型的函数。与其他语言不一样的是,这里会对类型进行推导

下面的两种形式都是会调用一个 float64max(a, b float64) 函数的

fmt.Println(max(float64(1), float64(2)))
fmt.Println(max[float64](1, 2))

一些其他的测试

多个类型与主动声明类型

package main

import "fmt"

type Number interface{
	type int, int8, int16, int32, int64, 
		uint, uint8, uint16, uint32, uint64,
		float32, float64
}

type Float interface{
	type float32, float64
}


func max[T Number, T2 Float] (a, b T) T {
	var ll T = 1
	var ll2 T2 = 3
	fmt.Printf("%+v %T %+v %T ", ll, ll, ll2,ll2)
	if a > b {
		return a
	}
	return b
}

func main() {
	fmt.Println(max[float64, float64](1, 2))
}

这里会输出

1 float64 3 float64 2

错误的类型

package main

import "fmt"

type Number interface{
	type int, int8, int16, int32, int64, 
		uint, uint8, uint16, uint32, uint64,
		float32, float64, string
}



func max[T Number] (a, b T) T {
	if a > b {
		return a
	}
	return b
}

func main() {
	fmt.Println(max(1, 2))
}

这里会输出

2

但是如果代码中存在诸如 var c T = 2 的代码,则会报错,因为 2 无法赋值给 string(即使没有 max[string](a, b string) 的调用也会报错)

看起来,只有赋值会被检查,但是 + 运算本身不会被检查?

泛型类

package main

import "fmt"

type Number interface{
	type int, int8, int16, int32, int64, 
		uint, uint8, uint16, uint32, uint64,
		float32, float64, string
}

type container [T Number]struct {
	l []T
}

func newContainer[T Number] () container[T]  {
	return container [T]{
		l : make([]T, 0),
	}
}

func (c container [T])Append(x T) {
	c.l = append(c.l, x)
}

func main() {
	c := newContainer[int]()
	c.Append(1)
	fmt.Printf("%+v, %T, %T", c, c, c.l)
}

这里依旧是需要确保泛型不会被导出,类、函数都需要是小写开头
输出

{l:[]}, main.container[int], []int

总结

无论怎么说,有泛型就是极大的进步,虽然别的地方能用优雅解释,但是没人会觉得 int(math.Max(1, 2)) 优雅

新引入的泛型还存在一些很奇怪的问题(比如上面提到的对于类型检查行为的不一致),不过下个版本正式发布应该会有修复

同时,Go 的泛型,不支持导出到包外部,也即无法首字母大写,这可以进一步避免泛型的滥用,但是如果不能导出,那么使用泛型的意义是什么?比如实现了一个哈希表、链表,这时必然需要可导出的泛型