1.18新增两种泛型定义语法,泛型函数和泛型约束集
泛型函数
声明如下:
func F[T C](v T) (T,error) {
...
}
中括号定义泛型,T表示类型参数,C表示类型集(也叫类型约束)。
泛型类型
type S[T C] struct {
v T
}
T是类型参数,C是类型集(类型约束)。
泛型类型集
type I[T C] interface {
~int | ~int32 | ~int64
M(v T) T
}
类型集是接口的扩展。
新增关键字
any
interface{}any
// any is an alias for interface{} and is equivalent to interface{} in all ways.
type any = interface{}
anyinterface{}anyinterface{}
comparable
mapkey
// 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
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 keycomparable
~
~time.Durationint64
// 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
int64int64~int64int64time.Duration
例:
func sumGeneric[T ~int | float32 | ~int64 | float64 | string](ns ...T) (sum T) {
for _, v := range ns {
sum += v
}
return sum
}
Ordered
不算是关键字,属于标准库的一个预置类型,表示可排序约束,即可使用<,<=,>,>=预算的类型。
// 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
}
例:
type Or[T constraints.Ordered] struct {
num T
}
泛型使用
泛型函数
简单示例
我们从最简单的计算和的函数开始。
在有泛型之前,如果需要计算数组的和需要写多个函数:
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
}
函数内部是完全重复的代码,但是不同的类型就需要编写不同的函数,非常浪费心智,且造成代码重复率过高。
使用泛型可以解决这个问题:
func GenericSum[T int | float64](elems ...T) (sum T) {
for _, v := range elems {
sum += v
}
return sum
}
sumGenericintfloat32
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
}
如果我们想扩充数据,则可以在类型集中添加更多类型:
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]
多类型和多参数函数
我们可以同时支持多个模板类型,用于多参数函数:
// 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
// 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]
}
泛型方法
目前还不支持泛型方法,但支持通过泛型类型定义泛型方法:
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的定义不能自行推导,需要显示定义类型,因此使用起来有部分局限性
x := X{u: "hello"} // '"hello"' (type string) cannot be represented by the type U
泛型类型集
泛型类型集是使用公理化集合论方法扩展了原有接口的定义,从而实现了泛型的类型约束。
简化函数签名
类型集支持将多种类型重定义为一个类型,可简化下面的函数定义:
func GenericSum[T ~int | float32 | ~int64 | float64 | string](ns ...T) (sum T)
sumT
type sumT interface {
~int | float32 | ~int64 | float64 | string
}
func GenericSum[T sumT](elems ...T) (sum T) {
for _, v := range elems {
sum += v
}
return sum
}
实现特定约束
// 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) }
注意,类型集是符合集合论的运算规则的,比如,取交集,并集等,因此我们可以设计一些无法实例化,无法使用的类型集:
type A interface {
int | string
float64
}
type B interface {
int
String()string
}
为保证编译速度,减少编译解析的时间复杂度,规定
并集元素中不能包含具有方法集的参数类型
如:
type S interface {
string | fmt.Stringer // ERROR:cannot use fmt.Stringer in union (fmt.Stringer contains methods)
}
fmt.Stringer
泛型限制
- 不支持变长类型参数:
type S[Ts ...comparable] struct {
elems ...Ts
}
- 不支持泛型函数内部定义类型
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://github.com/samber/lo slice,map,channel的各种操作
泛型Q&A泛型性能
go generate
泛型为什么使用中括号
( )[ ]{ }< >
- 尖括号
尖括号是很多语言的泛型选择,比如Java,C++,C#等。那么为什么Golang不选用此方案呢?可以观察下面语句:
a, b = w < x, y > (z)
a = w < xb = y > (z)a,b = w(z)>(?=xxx)
- 花括号
Golang中使用花括号来划分代码块、复合字面量(composite literals)和一些复合类型,因此几乎不可能在没有严重语法问题的情况下将花括号用于泛型。
- 小括号
type
struct{ (T(int)) }
interface{ (T(int)) }
- 中括号
中括号和小括号类似,会存在冲突歧义,主要是在切片,Map和数组定义中存在,为了解决歧义,在定义时需添加现在我们看到的类型参数。同时,中括号在定义时比小括号更简洁。并且在1.18之前版本的Golang中,切换和Map的定义都可以广义的认为是泛型切片,泛型Map的一种特例,从而实现了风格统一。
泛型设计
泛型设计有多态和单态两种设计思路。
虚拟方法表vtable
单态模式则是为每个独特的操作对象创建一个函数副本,主要工作都是在编译阶段。
多态的问题就是运行时开销比单态更多,而单态则是用更长的编译时间来换取结果代码的性能提升。
generate
什么时候应该使用泛型
使用泛型
泛型主要用来降低代码重复率,比如上面的Sum函数。
比如https://github.com/samber/lo库实现的内置类型操作。
不使用泛型
如果既可以使用类型参数,也可以使用接口参数,那么不应该考虑使用泛型
如:
type Ib[T any] interface {
Foo()
}
func bar2(T Ib[int]) {
T.Foo()
}
这里本意是传递参数需实现Foo方法,那么直接使用接口比泛型更简单易懂,不需要额外使用泛型语法。
总之,目前泛型实现是第一版,还有很多功能并不完备,因此使用泛型需要克制,尽量使用官方库。