golang拾遗:为什么我们需要泛型( 四 )

这是测试代码(golang1.15.2):
const ArrLength = 500var _arr []interface{}var _uintArr []uintfunc init() { _arr = make([]interface{}, ArrLength) _uintArr = make([]uint, ArrLength) for i := 0; i < ArrLength - 1; i++ {_uintArr[i] = uint(rand.Int() % 10 + 2)_arr[i] = _uintArr[i] } _arr[ArrLength - 1] = uint(1) _uintArr[ArrLength - 1] = uint(1)}func BenchmarkIndexOfInterface(b *testing.B) { for i := 0; i < b.N; i++ {IndexOfInterface(_arr, uint(1)) }}func BenchmarkIndexOfInterfacePacking(b *testing.B) { for i := 0; i < b.N; i++ {IndexOfInterfacePacking(uint(1), _arr...) }}func indexOfUint(arr []uint, value uint) int { for i,v := range arr {if v == value {return i} } return -1}func BenchmarkIndexOfUint(b *testing.B) { for i := 0; i < b.N; i++ {indexOfUint(_uintArr, uint(1)) }}func BenchmarkIndexOfByReflectInterface(b *testing.B) { for i := 0; i < b.N; i++ {IndexOfByReflect(_arr, uint(1)) }}func BenchmarkIndexOfByReflectUint(b *testing.B) { for i := 0; i < b.N; i++ {IndexOfByReflect(_uintArr, uint(1)) }}
golang拾遗:为什么我们需要泛型文章插图
我们吃惊地发现 , 直接使用interface比原生类型慢了10倍 , 如果使用反射并接收原生将会慢整整100倍!
另一个使用接口的例子是比较slice是否相等 , 我们没有办法直接进行比较 , 需要借助辅助手段 , 在我以前的这篇博客有详细的讲解 。 性能问题同样很显眼 。
复合类型的迷思interface{}是接口 , 而[]interface{}只是一个普通的slice 。 复合类型中的接口是不存在协变的 。 所以下面的代码是有问题的:
func work(arr []interface{}) {}ss := []string{"hello", "golang"}work(ss)类似的问题其实在前文里已经出现过了 。 这导致我们无法用interface统一处理slice , 因为interface{}并不是slice , slice的操作无法对interface使用 。
为了解决这个问题 , golang的sort包给出了一个颇为曲折的方案:
golang拾遗:为什么我们需要泛型文章插图
sort为了能处理slice , 不得不包装了常见的基本类型的slice , 为了兼容自定义类型包里提供了Interface , 需要你自己对自定义类型的slice进行包装 。
这实现就像是千层饼 , 一环套一环 , 即使内部的quicksort写得再漂亮性能也是要打不少折扣的 。
最后也是最重要的对于获取接口类型变量的值 , 我们需要类型断言 , 然而类型断言是运行时进行的:
var i interface{}i = 1s := i.(string)这会导致panic 。 如果不想panic就需要第二个变量去获取是否类型断言成功:s, ok := i.(string) 。
然而真正的泛型是在编译期就能发现这类错误的 , 而不是等到程序运行得如火如荼时突然因为panic退出 。
泛型带来的影响 , 以及拯救彻底从没有泛型的泥沼中解放同样是上面的IndexOf的例子 , 有了泛型我们可以简单写为:
package mainimport ( "fmt")func IndexOf[T comparable](arr []T, value T) int {for i, v := range arr {if v == value {return i}}return -1}func main() { q := []uint{1,2,3,4,5} fmt.Println(IndexOf(q, 5))}comparable是go2提供的内置设施 , 代表所有可比较类型 , 你可以在这里运行上面的测试代码 。
泛型函数会自动做类型推导 , 字面量可以用于初始化uint类型 , 所以函数正常运行 。
代码简单干净 , 而且没有性能问题(至少官方承诺泛型的绝大部分工作会在编译期完成) 。
再举个slice判断相等的例子:
func isEqual[T comparable](a,b []T) bool {if len(a) != len(b) {return false;}for i := range a {if a[i] != b[i] {return false}}return true}除了大幅简化代码之外 , 泛型还将给我们带来如下改变:

  • 真正的类型安全 , 像isEqual([]int, []string)这样的代码在编译时就会被发现并被我们修正
  • 虽然泛型也不支持协变 , 但slice等复合类型只要符合参数推导的规则就能被使用 , 限制更少
  • 没有了接口和反射 , 性能自不必说 , 编译器就能确定变量类型的话还可以增加代码被优化的机会
可以说泛型是真正救人于水火 。 这也是泛型最终能进入go2提案的原因 。
泛型的代价最后说了这么多泛型的必要性 , 也该是时候谈谈泛型之暗了 。
其实目前golang的泛型还在提案阶段 , 虽然已经有了预览版 , 但今后变数还是很多 , 所以这里只能针对草案简单说说两方面的问题 。
第一个还是类型系统的割裂问题 , golang使用的泛型系统比typwscript更加严格 , any约束的类型甚至无法使用赋值运算之外的其他内置运算符 。 因此想要类型能比较大小的时候必定创建自定义类型和自定义的类型约束 , 内置类型是无法添加方法的 , 所以需要包装类型 。