Go 1.17 泛型尝鲜

今天,Go的1.17版本终于正式发布,除了带来各种优化和新功能外,1.17正式在程序中提供了尝鲜的泛型支持,这一功能也是为1.18版本泛型正式实装做铺垫。意味着在6个月后,我们就可以正式使用泛型开发了。那在Go 1.18正式实装之前,我们在1.17版本中先尝鲜一下泛型的支持吧。
泛型有什么作用?
在使用Go没有泛型之前我们怎么实现针对多类型的逻辑实现的呢?有很多方法,比如说使用interface{}
作为变量类型参数,在内部通过类型判断进入对应的处理逻辑;将类型转化为特定表现的鸭子类型,通过接口定义的方法实现逻辑整合;还有人专门编写了Go的函数代码生成工具,通过批量生成不同类型的相同实现函数代替手工实现等等。这些方法多多少少存在一些问题:使用了interface{}
作为参数意味着放弃了编译时检查,作为强类型语言的一个优势就被抹掉了。同样,无论使用代码生成还是手工书写,一旦出现问题,意味着这些方法都需要重复生成或者进行批量修改,工作量反而变得更多了。
在Go中引入泛型会给程序开发带来很多好处:通过泛型,可以针对多种类型编写一次代码,大大节省了编码时间。你可以充分应用编译器的编译检查,保证程序变量类型的可靠性。借助泛型,你可以减少代码的重复度,也不会出现一处出现问题需要修改多处地方的尴尬问题。这也让很多测试工作变得更简单,借助类型安全,你甚至可以少考虑很多的边缘情况。
Go语言官方有详细的泛型提案文档可以在这里和这里查看详情。
如何使用泛型
前面理论我们仅仅只做介绍,这次尝鲜还是以实践为主。让我们先从一个小例子开始。
从简单的例子开始
让我们先从一个最简单的例子开始:
这个函数可以实现任何需要使用+
符号进行运算的类型,我们通过定义Addable
类型,枚举了所有可能可以使用add
方法的所有的类型。比如我们在main
函数中就使用了int
和string
两种不同类型。
但是如果这时我们使用简单的go run
命令运行,会发现提示语法错误:
因为在Go 1.17中,泛型并未默认开启,你需要定义gcflags
方式启用泛型:
如果你觉得这种方式太过于复杂,每次都需要添加,也可以通过定义环境变量形式让每次都带此参数(不推荐,尤其是多版本环境时低版本Go中会报错):
在Go中,泛型可以做什么更多更复杂的事情吗?当然可以。除了最基础的算法实现以外,我们可以通过后面的几个场景看一下泛型可用的场景。
实现类型安全的Map
在现实开发过程中,我们往往需要对slice中数据的每个值进行单独的处理,比如说需要对其中数值转换为平方值,在泛型中,我们可以抽取部分重复逻辑作为map函数:
在这个例子中,我们定义了一个M类型,因此除了进行同样类型的转换外,也可以做不同类型的转换:
实现类型安全的Map/Filter
除了操作数据以外,我们通常还需要对数据进行筛选。在前面的例子上,我们可以通过实现filterFunc
实现更好的通用逻辑:
实现类型可靠的Worker Pool
除了上面这个例子,我们还可以通过泛型实现一个类型可靠的通用批量类型转换函数:
其他应用
我们可以预见在Go 1.18版本中,多个标准库会被新增或者扩展,包括:类型定义库constraints
,通用slice操作库slices
,通用类型安全mapmaps
等等。因为这些会进入标准库,大家可以先自行实现试用,真正线上使用建议等待标准库添加内容即可。
Go泛型的实现原理
我们回归到最原始的例子快速看一下Go中是如何实现泛型的。为了方便分析,我们在所有func
上添加go:noinline
防止内联,然后编译程序进行分析。这里可能Go 1.17实现问题未能支持如go tool
或go build -gcflags=all=-S
之类的命令传递-G=3
参数,因此这里我们选择第三方的反汇编工具看一下具体的实现:

可以看到目前Go会根据类型将泛型展开成对应类型函数,这样也会小小的增加编译时间和编译后文件大小。因为我测试使用Apple Silicon平台,考虑大家可能不熟悉相关汇编,具体执行逻辑不再具体展示。
其他注意事项
目前Go的泛型仍在开发过程中,即便在1.17beta到正式版过程中,很多泛型的corner case也正在完善过程中,比如在之前测试中我发现某些代码在beta版本无法正确编译,但是在RC中已可以正确编译。目前的泛型实现未必代表1.18版本中是相同的实现细节,甚至可能在1.18中提供更多的功能。同时,目前1.17泛型类型是无法在package中导出的,这导致在1.17版本中它的应用场景大大的受限。如果你仍有计划在某些场景中使用,我仍旧建议单元测试覆盖你使用的场景情况,防止出现版本迭代可能导致的问题。