go泛型初探

0.前言

前不久Go的主线版本合并了泛型Feature,此前对泛型的呼声很高,泛型会带来强大的扩展能力,看C++的STL库就可见一斑。

动态类型语言通常没有这方面的需求,因为对于像python来说,他的函数的入参类型可以是任意的,像Go、C++这种静态类型语言,则需要编译器的支持来实现泛型。这篇文档准备来尝试一下主线版本的泛型功能。但是在此之前,先来考虑一个问题

1.泛型和interface什么关系与区别?

很遗憾在我写C++代码时没有考虑过这个问题,因为与go的interface对应的,往往被认为是void*。但void*并不具备反射的能力。所以到使用Go开发时,这个问题就冒出来了。
根据Why Generic?这篇文章的介绍,interface其实是一种泛型能力。

In other words, interface types in Go are a form of generic programming. They let us capture the common aspects of different types and express them as methods. We can then write functions that use those interface types, and those functions will work for any type that implements those methods.

就像C++的面向对象也是一种泛型一样。但面向对象在编码阶段终究还是有类型的,强类型语言通常会在编译器进行类型校验。你无法做到一个函数对所有的类对象都通用。所以这也是一种不彻底的泛型。

根据《泛型编程》一书的介绍,泛型是指彻底的抛弃类型的束缚,使过程脱离开具体对象。换言之,如果你要使用interface来替代泛型,也不是不可以。但是你要通过type interface来自定义高度抽象的类型,并在实现类中写很多重复代码,或者通过在过程函数中,植入大量的反射代码来实现多类型的支持。

泛型可以把这些重复代码的生成,交给编译器推导生成,这就是泛型存在的意义。它使得静态类型语言可以像动态类型语言一样,不关心类型信息地实现逻辑。下面通过具体的示例来看一下,首先看下泛型函数的语法

2.泛型函数的语法

func reverse[T any](array []T)

这里声明了一个名字为reverse的方法,方括号部分是泛型函数中的抽象类型定义,如果会用到多个类型,则以逗号分割。而函数的入参为该类型的切片。函数用于将切片中的内容进行逆序。

如果了解过C++的模板定义语法,这就一点也不陌生,不确定Go的模板参数是否会像C++一样支持默认参数。

3.泛型函数的应用

下面是reverse的实现以及调用代码,可以看到除了函数的定义上有点特殊,调用方式与普通函数无异。如果你需要修改自己的库函数为泛型形式,应用代码几乎是不需要改动的,这是很重要的。

package main

import "fmt"

func reverse[T any](array []T){
	left := 0
	right:= len(array)-1
	for left < right{
		array[left], array[right] = array[right], array[left]
		left++
		right--
	}
}

func main() {
	intArr := []int{1,2,3}
	strArr := []string{"hello","world"}

	reverse(intArr)
	fmt.Println(intArr)

	reverse(strArr)
	fmt.Println(strArr)
}

4.主线go版本的使用

如果你要自己clone主线代码来编译,那么要额外做很多工作,免不了要踩坑,因为主线go是未发布的go版本。因此,官方提供了自助编译主线go的工具,具体参见gotip。只需要两个命令即可获得gotip工具。

gotip命令的使用与go命令一致(因为它就是go命令的最新版呀),但是并不支持build子命令。如果你直接执行gotip build来编译上面👆🏻的源码,则会报如下错误。

$ gotip build
# generics
./main.go:17:12: type parameters require go1.18 or later
./main.go:17:15: undeclared name: any (requires version go1.18 or later)
./main.go:24:2: undefined: Reserve
./main.go:26:7: implicit function instantiation requires go1.18 or later
./main.go:27:7: implicit function instantiation requires go1.18 or later
./main.go:28:7: implicit function instantiation requires go1.18 or later
./main.go:29:7: implicit function instantiation requires go1.18 or later

(显然官方打算在1.18版本发布泛型)这时需要使用gotip run main.go来执行,如果没有异常,输出如下:

$ gotip run main.go
[3 2 1]
[world hello]

可以看到int和string类型的切片,在同一个函数中被翻转了。而如果要把一个普通函数转换为一个泛型函数,只需要在函数签名上做一丢丢改动,就可以具备这样的能力。

5.泛型背后的工作

go在背后做了什么工作,使得我们可以实现上面的能力呢?
这个核心就是编译期对泛型函数的展开,展开来说,先执行如下命令:

$ gotip tool compile -S -N main.go > main.s

可以获取到我们go程序的反汇编代码,这里截取一段来观察

"".main STEXT size=1850 args=0x0 locals=0x1d0 funcid=0x0 align=0x0
	0x0000 00000 (main.go:15)	TEXT	"".main(SB), ABIInternal, $464-0
	0x0000 00000 (main.go:15)	LEAQ	-336(SP), R12
	...
	0x0129 00297 (main.go:17)	MOVQ	DX, "".strArr+312(SP)
	0x0131 00305 (main.go:17)	MOVQ	$2, "".strArr+320(SP)
	0x013d 00317 (main.go:17)	MOVQ	$2, "".strArr+328(SP)
	0x0149 00329 (main.go:19)	LEAQ	""..dict.reverse[int](SB), DX
	0x0150 00336 (main.go:19)	MOVQ	DX, ""..dict+96(SP)
	0x0155 00341 (main.go:19)	MOVQ	"".intArr+336(SP), DX
	0x015d 00349 (main.go:19)	MOVQ	"".intArr+344(SP), SI
	...
	0x03ba 00954 (main.go:20)	MOVQ	fmt..autotmp_0+128(SP), DX
	0x03c2 00962 (main.go:20)	MOVQ	DX, fmt.n+64(SP)
	0x03c7 00967 (main.go:20)	MOVQ	fmt..autotmp_1+296(SP), DX
	0x03cf 00975 (main.go:20)	MOVQ	fmt..autotmp_1+304(SP), R8
	0x03d7 00983 (main.go:20)	MOVQ	DX, fmt.err+200(SP)
	0x03df 00991 (main.go:20)	MOVQ	R8, fmt.err+208(SP)
	0x03e7 00999 (main.go:20)	JMP	1001
	0x03e9 01001 (main.go:22)	LEAQ	""..dict.reverse[string](SB), DX
	0x03f0 01008 (main.go:22)	MOVQ	DX, ""..dict+88(SP)
	0x03f5 01013 (main.go:22)	MOVQ	"".strArr+312(SP), DX
	0x03fd 01021 (main.go:22)	MOVQ	"".strArr+320(SP), SI
	0x0405 01029 (main.go:22)	MOVQ	"".strArr+328(SP), R8
	...
"".reverse[%2eshape.int_0] STEXT dupok nosplit size=281 args=0x20 locals=0x38 funcid=0x0 align=0x0
	0x0000 00000 (main.go:5)	TEXT	"".reverse[%2eshape.int_0](SB), DUPOK|NOSPLIT|ABIInternal, $56-32
	0x0000 00000 (main.go:5)	SUBQ	$56, SP
	0x0004 00004 (main.go:5)	MOVQ	BP, 48(SP)
	0x0009 00009 (main.go:5)	LEAQ	48(SP), BP
	0x000e 00014 (main.go:5)	FUNCDATA	$0, gclocals·09cf9819fc716118c209c2d2155a3632(SB)
	0x000e 00014 (main.go:5)	FUNCDATA	$1, gclocals·69c1753bd5f81501d95132d08af04464(SB)
	0x000e 00014 (main.go:5)	FUNCDATA	$5, "".reverse[%2eshape.int_0].arginfo1(SB)
	0x000e 00014 (main.go:5)	MOVQ	AX, ""..dict+64(SP)
	0x0013 00019 (main.go:5)	MOVQ	BX, "".array+72(SP)
	...
"".reverse[%2eshape.string_0] STEXT dupok size=446 args=0x20 locals=0x40 funcid=0x0 align=0x0
	0x0000 00000 (main.go:5)	TEXT	"".reverse[%2eshape.string_0](SB), DUPOK|ABIInternal, $64-32
	0x0000 00000 (main.go:5)	CMPQ	SP, 16(R14)
	0x0004 00004 (main.go:5)	PCDATA	$0, $-2
	0x0004 00004 (main.go:5)	JLS	395
	0x000a 00010 (main.go:5)	PCDATA	$0, $-1
	0x000a 00010 (main.go:5)	SUBQ	$64, SP
	0x000e 00014 (main.go:5)	MOVQ	BP, 56(SP)
	0x0013 00019 (main.go:5)	LEAQ	56(SP), BP
	0x0018 00024 (main.go:5)	FUNCDATA	$0, gclocals·09cf9819fc716118c209c2d2155a3632(SB)
	0x0018 00024 (main.go:5)	FUNCDATA	$1, gclocals·2589ca35330fc0fce83503f4569854a0(SB)
	0x0018 00024 (main.go:5)	FUNCDATA	$5, "".reverse[%2eshape.string_0].arginfo1(SB)

可以看到只定义了一个reverse方法,编译器基于该泛型函数(main.go:5)为我们生成了两个不同签名的reverse函数。这个过程就是泛型函数的展开。

在main方法中的第19行和22行,分别调用了这两个方法。编译器根据我们的调用代码,自动推导出了需要生成的方法代码。所以泛型函数的声明,有时也可以看成非原生的Go代码,它更像是一种代码生成器语法。

6.结语

综上,go中引入泛型是一个值得兴奋的事情,但是似乎还有很长的路要走,在官方的maillist中也表示过,1.18版本可能会无法满足所有泛型test case。另外,泛型展开也会使编译时间变得更长,是否做第一批吃螃蟹的人还请同学们自行斟酌。

7.链接

Why Generic?
Type parameters design document

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值