Golang 中的泛型|青训营笔记

210 阅读2分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天

介绍

在编程过程中往往会遇到需要对多个类型实现相同功能的函数、类或接口的场景。通常的做法是对每个类型重复实现功能相同的代码块,这些代码块的逻辑功能相同,仅仅只是类型不同。

例如实现切片插入操作。

func SliceInsertInt(s []int, i int, vs []int) []int {
	return append(s[:i], append(vs, s[i:]...)...)
}

func SliceInsertString(s []string, i, int, vs []string) []string {
	return append(s[:i], append(vs, s[i:]...)...)
}

对多个类型实现仅类型不同而功能相同的代码块无疑造成了代码的冗余。

另一种可行的做法是使用反射(reflect)。

这种做法虽然不需要重复定义函数,但由于使用了反射,代码的执行效率较低,且不符合 Golang 静态类型的设计思想,在复杂场景中使用容易出现问题。泛型(Generics)为这种实际问题提供了一种良好的解决方案。

泛型是一种语言特性,是指在定义函数、类或接口时不预先指定具体的类型,而是在使用时再指定。Golang 在 1.18 版本支持了这种特性。

之前的问题使用泛型可以这样写。

func SliceInsert[T any](s []T, i int, vs []T) []T {
	return append(s[:i], append(vs, s[i:]...)...)
}

泛型定义

基本的泛型声明可以表示为如下形式

函数、类或接口名[泛型变量名 泛型变量类型约束...]

类似变量的声明,泛型声明包括泛型变量名和泛型类型约束,并使用 [] 进行包括。

一般情况下,泛型变量可以根据应用场景随意命名,通常约定使用 TKV 等大写字母来代表。

泛型类型就是约束了泛型变量可以作为的数据类型,可以是基本数据类型,也可以是自定义的类型。且由于其本质上是一个 interface,所以也可以在类型约束复杂时单独进行声明。

type Type interface {
	int | string | float32
}

关于 Golang 泛型类型约束的高级用法可以参考 Constraints and Type Parameters

使用示例

结构体中的泛型

type List[T any] struct {
	head, tail *element[T]
}
type element[T any] struct {
	next *element[T]
	val T
}

函数中的泛型

func MapKeys[K comparable, V any](m map[K][V]) []K {
	r := make([]K, 0, len(m))
	for k := m {
		r = append(r, k)
	}
	return r
}

方法中的泛型

// type and method define
type MyFloat float64

func (mf MyFloat) String() string {
	return fmt.Sprintf("float: %f", mf)
}

type MyInt int

func (mi MyInt) String() string {
	return fmt.Sprintf("int: %d", mi)
}

// generics
type Constraint interface {
	~int | ~float64 // type
	String() string // method
}

func ConvertSliceToSlice[T Constraint](s []T) []string {
	o := make([]string, 0, len(s))
	for _, v := range s {
		o = append(o, v.String())
	}
	return o
}