泛型定义

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方法,那么直接使用接口比泛型更简单易懂,不需要额外使用泛型语法。

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