Golang在1.18版本支持了泛型,写过java/c++等语言的可能对泛型有一定的了解。那么泛型到底是什么呢?他有什么作用呢?
为什么需要泛型
为什么需要泛型呢?Golang是强类型语言,任何变量或者函数参数,都需要定义明确的参数类型。假设我们需要实现这么一个函数,输入两个参数,函数返回其相加的值,输入参数可以是两个整型int,浮点数float,还有可能是字符串等等,这时候通常怎么办?定义多个函数实现吗?如下面程序所示:
//定义多个函数实现
func twoIntValueSum(a, b int) int {
return a + b
}
func twoFloatValueSum(a, b float32) float32 {
return a + b
}
func twoStrValueSum(a, b string) string {
return a + b
}
//定义一个函数,类型是interface{}
这样就可能导致存在大量重复代码,而且调用方还需要根据参数类型决定调用哪一个方法。还能怎么办呢?只定义一个函数,只是参数是interface{},函数内部通过反射等方式,执行对应的操作,如下面程序:
func twoValueSum(a, b interface{}) (interface{}, error) {
if reflect.TypeOf(a).Kind() != reflect.TypeOf(b).Kind() {
return nil, errors.New("two value type different")
}
switch reflect.TypeOf(a).Kind() {
case reflect.Int:
return reflect.ValueOf(a).Int() + reflect.ValueOf(b).Int(), nil
case reflect.Float64:
return reflect.ValueOf(a).Float() + reflect.ValueOf(b).Float(), nil
case reflect.String:
return reflect.ValueOf(a).String() + " " + reflect.ValueOf(b).String(), nil
default:
return nil, errors.New("unknow value type")
}
}
使用反射实现的话,依赖反射性能较低,二来可以看到输入参数和返回值都是interface{},使用方还需要多执行一步返回值类型转换,而且反射相对而言还是比较复杂的。
泛型初体验
那么还有其他什么办法吗?这就要说到Go 1.18版本实现的泛型了,泛型相当于定义了一个函数模板,真正调用函数的时候,再确定参数以及返回值等具体类型,基于泛型实现上述功能如下:
package main
import "fmt"
func main() {
ret := twoValueSum[int](100, 200)
fmt.Println(ret)
ret1 := twoValueSum[string]("hello ", "world")
fmt.Println(ret1)
}
func twoValueSum[T int | float64 | string](a T, b T) T {
return a + b
}
泛型类型或者泛型函数定义的语法格式可以描述为[Identifier TypeConstraint],上述程序中的T就是标识符(Identifier),int等就是TypeConstraint(类型限制,也就是说twoValueSum函数的输入参数类型只能是这几种,不能是其他的),注意在调用具体函数时,需要声明真正的类型。
下面举几个泛型程序事例,介绍下泛型类型常见定义方式(泛型函数参考twoValueSum函数定义):
//定义切片类型,元素类型可以是int,float64或者string
type Slice[T int|float64|string ] []T
//实例化变量arr1,注意声明了切片元素类型为int
var arr1 Slice[int] = []int{1, 2, 3}
//实例化变量arr2,注意声明了切片元素类型为string
var arr2 Slice[string] = []string{"Hello", "World"}
//自定义map类型,key只能是string,value可以是int、float32或者float64
type DefineMap[KEY string, VALUE int | float32 | float64] map[KEY]VALUE
var m DefineMap[string, int] = map[string]int{
"zhangsan": 89,
"lisi": 80,
}
再举一个例子,假设想实现一个切片比较的函数(类似于字符串的字典序比较),该怎么定义呢?参数可以是[]int,[]float,[]string等等,只要元素可比较即可。参考官方给的事例:
//定义浮点数类型集合,
type Float interface {
~float32 | ~float64
}
//定义有符号整数集合
type Signed interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
//定义无符号整数集合
type Unsigned interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
//定义整数集合
type Integer interface {
Signed | Unsigned
}
//定义可比较类型集合:整数,浮点数,字符串
type Ordered interface {
Integer | Float | ~string
}
//Compare函数需要输入两个切片,切片元素类型必须是可排序的,这里限制为类型Ordered
func Compare[E Ordered](s1, s2 []E) int
//二分法实现函数,输入切片以及查找元素,切片元素类型必须是可排序的,这里限制为类型Ordered
func BinarySearch[E Ordered](x []E, target E) (int, bool)
注意目前Go语言标准库还没有使用泛型(Go作者不建议),不过有几个实验库使用了泛型,有兴趣的读者可以查阅:
golang.org/x/exp/constraints
Constraints that are useful for generic code, such as constraints.Ordered.
golang.org/x/exp/slices
A collection of generic functions that operate on slices of any element type.
golang.org/x/exp/maps
A collection of generic functions that operate on maps of any key or element type.
另外,注意符号 ~ ,这是什么意思呢?假设我们定义一个泛型切片类型限制包含int,另外还有一个自定义类型(其实也是int),自定义类型能用来构造该切片吗?如下:
type Slice[T int | float64 | string] []T
type Integer int
var arr Slice[Integer] = []Integer{1,2,3}
//Integer does not implement int|float64|string (possibly missing ~ for int in constraint int|float64|string)
注意虽然自定义类型Integer其实也就是int,但是这两种类型是不相等的,所以这里才有语法错误"Integer does not implement int",针对这种情况,Go语言给出的建议是使用符号 ~ 定义,如:
type Slice[T ~int| ~ float64 | ~string ] []T
type Integer int
//编译通过
var arr Slice[Integer] = []Integer{1,2,3}
更多的泛型语法,以及使用场景,有兴趣的读者继续研究,这里就不一一介绍了。
泛型函数底层是怎么实现的
最后再思考一个问题,Go语言是如何实现上述泛型事例呢?为什么只定义一个函数实现,就能传递多种类型参数呢?为什么只定义一种变量类型,却能实例化多种类型的变量呢?
我们以下面的程序为例,看一下编译后的汇编代码,就明白其实现原理了:
package main
import "fmt"
func main() {
ret := twoValueSum[int](100, 200)
fmt.Println(ret)
ret1 := twoValueSum[string]("hello ", "world")
fmt.Println(ret1)
}
func twoValueSum[T int | float64 | string](a T, b T) T {
return a + b
}
//go tool compile -S -N -l test.go
"".main STEXT
//main函数中的函数调用替换了!
0x0037 00055 (test.go:7) CALL "".twoValueSum[go.shape.int_0](SB)
0x00e4 00228 (test.go:10) CALL "".twoValueSum[go.shape.string_0]
//编译阶段生成的两个具体的函数
"".twoValueSum[go.shape.int_0] STEXT
"".twoValueSum[go.shape.string_0] STEXT
可以看到,我们只定义了一个函数实现twoValueSum,但是Go编译器为我们生成了两个具体的函数(因为我们调用了这两种函数实现),而针对twoValueSum的函数调用,也都在编译过程被替换了。
总结
需要注意的是,Golang虽然在1.18版本支持了泛型,但是还是不建议在标准库使用,毕竟这次代码变动较大,而且后续泛型可能还会有较大变动,所以线上使用泛型需谨慎。