前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >理解Golang的泛型

理解Golang的泛型

原创
作者头像
chandlerpan
修改2022-07-12 12:23:42
1.3K0
修改2022-07-12 12:23:42
举报
文章被收录于专栏:程序员的自我修养

泛型定义

1.18新增两种泛型定义语法,泛型函数和泛型约束集

泛型函数

声明如下:

代码语言:go
复制
func F[T C](v T) (T,error) {
    ...
}

中括号定义泛型,T表示类型参数,C表示类型集(也叫类型约束)。

泛型类型

代码语言:go
复制
type S[T C] struct {
	v T
}

T是类型参数,C是类型集(类型约束)。

泛型类型集

代码语言:go
复制
type I[T C] interface {
	~int | ~int32 | ~int64
	M(v T) T
}

类型集是接口的扩展。

新增关键字

any

为降低interface{}带来的糟糕阅读体验,新增了any关键字,它实际上是一种语法糖,定义如下:

代码语言:go
复制
// any is an alias for interface{} and is equivalent to interface{} in all ways.
type any = interface{}

从定义可以看出,anyinterface{}没有任何区别,仅仅是个别名,不过目前官方是推荐使用any替代interface{}的,可以简化代码。

comparable

可对比类型约束(可使用==作对比),可用于mapkey,此类型是在编译过程中展开定义

代码语言:go
复制
// comparable is an interface that is implemented by all comparable types
// (booleans, numbers, strings, pointers, channels, arrays of comparable types,
// structs whose fields are all comparable types).
// The comparable interface may only be used as a type parameter constraint,
// not as the type of a variable.
type comparable interface{ comparable }

注意:interface{}在新版本中不属于comparable类型(后续可能会兼容):

代码语言:go
复制
func Equal[T comparable](v1, v2 T) bool {
	return v1 == v2
}

func TestCom(t *testing.T) {
	var a1 interface{} = 1
	var a2 interface{} = 2
	assert.Equal(t, true, Equal(a1, a2)) // interface{} does not implement comparable
}

这里是一个设计问题,interface{}本身是可以作为map key的,但是泛型中暂时不能使用为comparable

~

~用来表示类型集的扩展类型,如time.Duration是一个int64的类型重定义:

代码语言:go
复制
// A Duration represents the elapsed time between two instants
// as an int64 nanosecond count. The representation limits the
// largest representable duration to approximately 290 years.
type Duration int64

泛型的类型集int64表示仅支持int64类型参数,但是~int64还可以同时表示所有由int64派生来的数据类型,就比如time.Duration

例:

代码语言:go
复制
func sumGeneric[T ~int | float32 | ~int64 | float64 | string](ns ...T) (sum T) {
	for _, v := range ns {
		sum += v
	}
	return sum
}

Ordered

不算是关键字,属于标准库的一个预置类型,表示可排序约束,即可使用<,<=,>,>=预算的类型。

代码语言:go
复制
// Ordered is a constraint that permits any ordered type: any type
// that supports the operators < <= >= >.
// If future releases of Go add new ordered types,
// this constraint will be modified to include them.
type Ordered interface {
	Integer | Float | ~string
}

例:

代码语言:txt
复制
type Or[T constraints.Ordered] struct {
	num T
}

泛型使用

泛型函数

简单示例

我们从最简单的计算和的函数开始。

在有泛型之前,如果需要计算数组的和需要写多个函数:

代码语言:go
复制
func sumInt(ns ...int) (sum int) {
	for _, v := range ns {
		sum += v
	}
	return sum
}
func sumFloat(ns ...float32) (sum float32) {
	for _, v := range ns {
		sum += v
	}
	return sum
}

函数内部是完全重复的代码,但是不同的类型就需要编写不同的函数,非常浪费心智,且造成代码重复率过高。

使用泛型可以解决这个问题:

代码语言:go
复制
func GenericSum[T int | float64](elems ...T) (sum T) {
	for _, v := range elems {
		sum += v
	}
	return sum
}

此时,我们的函数sumGeneric支持输入int或者float32的数据:

代码语言:go
复制
func TestGenericSum(t *testing.T) {
	assert.Equal(t, 5, GenericSum[int](1, 2, 2)) // 显示定义类型为 int
	assert.Equal(t, 5, GenericSum(1.0, 2.0, 2.0))    // 自动推导类型为float64
}

如果我们想扩充数据,则可以在类型集中添加更多类型:

代码语言:go
复制
func GenericSum[T ~int | float32 | ~int64 | float64 | string](elems ...T) (sum T) {
	for _, v := range elems {
		sum += v
	}
	return sum
}

其中~表示支持派生类型,如GenericSum([]time.Duration{time.Duration(1), time.Duration(4)}...)。如果进一步,想支持所有类型,则可以直接写作[T any]

多类型和多参数函数

我们可以同时支持多个模板类型,用于多参数函数:

代码语言:go
复制
// SliceMap 将数组 s 中的数据处理后输入到新数组中并返回
// 这里定义两种类型,表示允许输入一种类型,输出另一种类型
func SliceMap[T, R any](s []T, f func(T) R) []R {
	result := make([]R, len(s))
	for i, v := range s {
		result[i] = f(v)
	}
	return result
}
func TestSliceMap(t *testing.T) {
	ss := []string{"1", "2", "3"}
	ff := func(input string) int {
		v, _ := strconv.Atoi(input)
		return v
	}
	assert.Equal(t, []int{1, 2, 3}, SliceMap(ss, ff))
}

上面实现了输入字符串,输出整形的功能,其中any表示支持任何类型的输入。

除此之外,我们还需要一些内置复合类型的泛型定义,即在类型定义中声明类型参数,可以使用下面范式:

undefined

代码语言:go
复制
// Pick 随机选取数组中一个对象返回
// 波浪线表示包含所有基于此类型派生的新类型(即type定义新类型)
func Pick[E ~int64 | string, S []E](s S) (ret E) {
	if len(s) == 0 {
		return ret
	}
	m := make(map[int]E)
	for i, v := range s {
		m[i] = v
	}
	for _, v := range m {
		return v
	}
	return s[0]
}

泛型方法

目前还不支持泛型方法,但支持通过泛型类型定义泛型方法:

代码语言:go
复制
type X[U any]struct {
	u U
}
func (x X)Foo(v any){} 		// ERROR:cannot use generic type X[U any] without instantiation
func (x X[U])Bar(v any){} 	// OK
func (x X)Say[V any](v V){} // ERROR:Method cannot have type parameters

注意:X的定义不能自行推导,需要显示定义类型,因此使用起来有部分局限性

代码语言:go
复制
x := X{u: "hello"} // '"hello"' (type string) cannot be represented by the type U

泛型类型集

泛型类型集是使用公理化集合论方法扩展了原有接口的定义,从而实现了泛型的类型约束。

简化函数签名

类型集支持将多种类型重定义为一个类型,可简化下面的函数定义:

代码语言:go
复制
func GenericSum[T ~int | float32 | ~int64 | float64 | string](ns ...T) (sum T)

通过定义sumT来简化函数声明:

代码语言:go
复制
type sumT interface {
	~int | float32 | ~int64 | float64 | string
}
func GenericSum[T sumT](elems ...T) (sum T) {
	for _, v := range elems {
		sum += v
	}
	return sum
}

实现特定约束

代码语言:go
复制
// Ia 模板类型集,表示只能接收指针类型的参数类型
type Ia[T any] interface {
	*T
}
// 此声明会报错 -- 不能作为参数使用,无法实例化模板,必须用中括号表示泛型模板来告知编译器进行实例化
func bar1(v Ia[any]) {} // Interface includes constraint element '*T', can only be used in type parameters
// 可作为类型集合使用 -- 此方法只接受指针参数
func barA[E any, T Ia[E]](v T) { fmt.Println("barA", *v) }
// 限制只能输入int类型值的指针
func barAA[T Ia[int]](v T) { fmt.Println("barAA", *v) }
// 限制只能输入any类型值的指针,其他值需要先显示转换成any类型才能传参
func barAAA[T Ia[any]](v T) { fmt.Println("barAAA", *v) }

注意,类型集是符合集合论的运算规则的,比如,取交集,并集等,因此我们可以设计一些无法实例化,无法使用的类型集:

代码语言:go
复制
type A interface {
	int | string
	float64
}
type B interface {
	int
	String()string
}

为保证编译速度,减少编译解析的时间复杂度,规定

并集元素中不能包含具有方法集的参数类型

如:

代码语言:go
复制
type S interface {
    string | fmt.Stringer // ERROR:cannot use fmt.Stringer in union (fmt.Stringer contains methods)
}

这里的fmt.Stringer是一个接口,编译时需要遍历实现此接口的对象和类型,然后再进行泛型遍历生成,这就会导致编译复杂度大大提升,使编译速度变慢,golang对编译速度是非常看重的,因此增加了这个限制。

泛型限制

  • 不支持变长类型参数:
代码语言:javascript
复制
type S[Ts ...comparable] struct {
	elems ...Ts
}
  • 不支持泛型函数内部定义类型
代码语言:javascript
复制
func Equal[T comparable](v1, v2 T) bool {
	type a struct{} // ERROR:type declarations inside generic functions are not currently supported
	return v1 == v2
}
  • 元编程、非类型类型参数、柯里化(foo(3)(4))
  • 不能重载运算符,导致自定义类型不能做运算符运算

泛型库

官方库

https://golang.org/x/exp/constraints 定义基础约束类型,如有符号,无符号,浮点,可对比类型等

https://golang.org/x/exp/slices 实现slice的各种基础操作,如是否存在,拷贝,是否相等

https://golang.org/x/exp/maps 实现map的各种基础操作,如遍历,拷贝,清空等

三方库

https://golang.design/x/reflect 对象深拷贝

https://github.com/samber/lo slice,map,channel的各种操作

泛型Q&A

泛型性能

相比反射实现的代码,泛型通常会有x7倍的性能提升。但与go generate生成代码相比,性能下降约4%,这和泛型设计有关。

泛型为什么使用中括号

目前计算机常用四对单字符对称括号,分别是小括号 ( )、方括号[ ]、花括号{ }以及尖括号< >。我们一一分析:

  • 尖括号

尖括号是很多语言的泛型选择,比如Java,C++,C#等。那么为什么Golang不选用此方案呢?可以观察下面语句:

代码语言:javascript
复制
a, b = w < x, y > (z)

这里到底是a = w < x, b = y > (z)还是a,b = w(z)呢?单从这段代码来看,编译器无法确定是什么语义。解决这个问题需要有效的无界lookahead(即需要右边>有一个明确的边界(?=xxx))。但Golang的开发团队现在更希望让 Golang解析器保持足够的简单,所以这里可以说是编译器不想做的太复杂的决定。

  • 花括号

Golang中使用花括号来划分代码块、复合字面量(composite literals)和一些复合类型,因此几乎不可能在没有严重语法问题的情况下将花括号用于泛型。

  • 小括号

在设计之初,Golang团队确实是使用小括号作为泛型的预案,并且为了向后兼容,他们表示不得不在类型参数列表中引入type关键字。最后,他们在参数列表、复合字面量和嵌入类型中发现了额外的解析歧义,而这些歧义需要嵌套更多的小括号来解决。

代码语言:javascript
复制
struct{ (T(int)) }
interface{ (T(int)) }
  • 中括号

中括号和小括号类似,会存在冲突歧义,主要是在切片,Map和数组定义中存在,为了解决歧义,在定义时需添加现在我们看到的类型参数。同时,中括号在定义时比小括号更简洁。并且在1.18之前版本的Golang中,切换和Map的定义都可以广义的认为是泛型切片,泛型Map的一种特例,从而实现了风格统一。

泛型设计

泛型设计有多态和单态两种设计思路。

多态主要思路就是先进行堆上内容分配、再把相应的指针传递给函数。因为所有操作对象都转化成了指针,我们只需要指针操作就能了解这些对象在哪里。但也因为指针太多,我们还需要创建一份函数指针表,也就是大家常说的虚拟方法表vtable。Golang接口的多态就是这样实现的。

单态模式则是为每个独特的操作对象创建一个函数副本,主要工作都是在编译阶段。

多态的问题就是运行时开销比单态更多,而单态则是用更长的编译时间来换取结果代码的性能提升。

Golang使用的是一种被称为“GCShape stenciling with Dictionaries”的部分单态化技术。即Goalng会在编译阶段将泛型进行部分单态化,为什么说是部分呢,因为对于底层类型相同的数据类型,它只会生成一个单态函数,然后生成一份类型字典,在执行过程中通过类型字典生成具体类型,因此Goalng的泛型相比generate代码会有部分性能损失。

什么时候应该使用泛型

使用泛型

泛型主要用来降低代码重复率,比如上面的Sum函数。

比如https://github.com/samber/lo库实现的内置类型操作。

不使用泛型

如果既可以使用类型参数,也可以使用接口参数,那么不应该考虑使用泛型

如:

代码语言:javascript
复制
type Ib[T any] interface {
	Foo()
}
func bar2(T Ib[int]) {
	T.Foo()
}

这里本意是传递参数需实现Foo方法,那么直接使用接口比泛型更简单易懂,不需要额外使用泛型语法。

总之,目前泛型实现是第一版,还有很多功能并不完备,因此使用泛型需要克制,尽量使用官方库。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 泛型定义
    • 泛型函数
      • 泛型类型
        • 泛型类型集
          • 新增关键字
            • any
            • comparable
            • ~
            • Ordered
        • 泛型使用
          • 泛型函数
            • 简单示例
            • 多类型和多参数函数
          • 泛型方法
            • 泛型类型集
              • 简化函数签名
              • 实现特定约束
            • 泛型限制
              • 泛型库
                • 官方库
                • 三方库
            • 泛型Q&A
              • 泛型性能
                • 泛型为什么使用中括号
                  • 泛型设计
                  • 什么时候应该使用泛型
                    • 使用泛型
                      • 不使用泛型
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档