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]
就是声明了一个类型 T
,T
满足 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
(可以理解为一个存储类型的变量)
在这里可以对泛型执行任意的操作,比如如果泛型都是数字,那么可以进行加法或赋值一个数字,类型会被自动进行转换
泛型调用
最后就是调用一个支持泛型的函数。与其他语言不一样的是,这里会对类型进行推导
下面的两种形式都是会调用一个 float64
的 max(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 的泛型,不支持导出到包外部,也即无法首字母大写,这可以进一步避免泛型的滥用,但是如果不能导出,那么使用泛型的意义是什么?比如实现了一个哈希表、链表,这时必然需要可导出的泛型