社区长期高呼的泛型特性在Golang 1.18中终于正式发布,Go泛型实现与传统的C++有较大差异,更像Rust的泛型实现。本文详细介绍Golang泛型及其特性,包括泛型语法、类型参数、类型约束、类型近似以及constraints包提供内置类型等。
最近写Dao代码,利用泛型可让代码更简洁通用。为了更系统掌握,特整理了Go泛型相关内容。
泛型可以让更通用,无需给参数或返回值显示指定特定类型,如,写函数或结构体时,无需具体指定值的类型。具体值类型之后传入,从而避免大量冗余模板代码。
泛型的目标就是消除模板代码。举例:反转数组函数无需知道数组元素类型,但如果不采用泛型则不能保障类型安全,或给每个具体类型重试实现同样逻辑,增加维护成本。
Go1.18.0 引入新的语法支持类型元数据并定义类型约束。举例:
func main() {
fmt.Println(reverse([]int{1, 2, 3, 4, 5}))
}
// T 是类型参数,在函数体内像正常类型一样使用
// any 是类型约束,后面会介绍其他类型
func reverse[T any](s []T) []T {
l := len(s)
r := make([]T, l)
for i, ele := range s {
r[l-i-1] = ele
}
return r
}
函数名后[]
用于指定类型参数, 可以包括一组类型标识以及约束类型。这里T是类型参数,可用于函数参数和返回值类型。any是interface的别名,其定义为:type any = interface{}
。
定义好泛型函数后,调用时直接传入具体类型。我们也可以实例化泛型函数,然后再调用:
func main() {
// fmt.Println(reverse([]int{1, 2, 3, 4, 5}))
// 实例化泛型函数
reverseInt := reverse[int]
// 使用reverseInt进行调用
reverseInt([]int{1, 2, 3, 4, 5})
}
前面的调用方式,Golang编译器自动推断泛型类型,所以下面两种方式结果一致:
// 未传入类型
fmt.Println(reverse([]int{1, 2, 3, 4, 5}))
// 传入类型
fmt.Println(reverse[int]([]int{1, 2, 3, 4, 5}))
基于具体类型的实用函数实用泛型让代码更通用,下面看一些具体示例,包括Slice,Map,当然Channel也支持泛型:
// 泛型slice ForEach函数, 对每个元素执行一个特定函数.
// 通过实用泛型,使得ForEach功能更通用,适合更多类型.
func ForEach[T any](s []T, f func(ele T, i int , s []T)){
for i,ele := range s {
f(ele,i,s)
}
}
- Map
map需要两个类型,key和value类型,下面示例value没有限制,key需要满足comparable限制:
// keys 返回mapkey集合
// 这里 m 使用K 和 V作为泛型
// V 的约束为 any
// K的约束为 comparable,可以理解为任何支持 != 和 == 操作的类型,后面会讲解约束
func keys[K comparable, V any](m map[K]V) []K {
// 使用K类型和map长度创建分片
key := make([]K, len(m))
i := 0
for k, _ := range m {
key[i] = k
i++
}
return key
}
- 结构体类型参数
Go也支持使用类型参数定义struct。语法和函数类似,可以在struct的数据成员和方法上使用类型参数。
// T is type parameter here, with any constraint
type MyStruct[T any] struct {
inner T
}
// No new type parameter is allowed in struct methods
func (m *MyStruct[T]) Get() T {
return m.inner
}
func (m *MyStruct[T]) Set(v T) {
m.inner = v
}
方法仅能使用struct中定义的类型参数,不能使用新的类型参数。
- 嵌套类型参数
泛型类型可以嵌套在其他类型中,定义在函数或结构体的类型参数可传给任何其他带类型参数的类型。举例:
// Generic struct with two generic types
type Enteries[K, V any] struct {
Key K
Value V
}
// map需要key可比较,K声明为comparable约束
// 这里使用了嵌套参数类型
// 初始化Enteries[K,V] 作为返回值类型,它使用K,V作为参数
func enteries[K comparable, V any](m map[K]V) []*Enteries[K, V] {
// define a slice with Enteries type passing K, V type parameters
e := make([]*Enteries[K, V], len(m))
i := 0
for k, v := range m {
// 使用new关键字创建变量
newEntery := new(Enteries[K, V])
newEntery.Key = k
newEntery.Value = v
e[i] = newEntery
i++
}
return e
}
这里Enteries类型作为enteries函数的返回值,通过传入需要的类型被实例化。
## 类型约束
与C++不同,Go泛型仅允许执行interface内列举的操作,这里interface称为约束。编译器使用约束来确保为使用类型参数实例化值执行的所有操作在一定类型范围内。
举例,下面代码片段中,T仅支持string方法,如len()或其他字符串方法。
// Stringer is a constraint
type Stringer interface {
String() string
}
// Here T has to implement Stringer, T can only perform operations defined by Stringer
func stringer[T Stringer](s T) string {
return s.String()
}
## 预定类型作为约束
Go的新功能允许预定义类型(如int和string)实现约束中使用的interface。这些具有预定义类型的interface只能用作约束。
type Number {
int
}
不能在方法上使用这些预定义类型,因为预定义类型在这些已定义类型上没有方法。
type Number {
int
Name()string // int don’t have Name method
}
`|`操作可以定义联合类型,即定义多个具体类型作为预定义类型;
type Number interface {
int | int8 | int16 | int32 | int64 | float32 | float64
}
上面示例中,Number类型支持所有能够执行如` <,> +,-`算术操作的类型。
可以使用联合类型作为约束,定义泛型函数:
// T 是类型参数,支持上述联合类型的一种
// 具体联合类型参数应该能实现算术运算操作
func Min[T Number](x, y T) T {
if x < y {
return x
}
return y
}
## 近似类型
Go支持从预定义类型(如int,string)创建用户定义类型, `~` 操作符指定interface支持与底层相同的类型。举例,Point类型底层为int类型,则Number定义需要使用`~`操作符.
// Any Type with given underlying type will be supported by this interface
type Number interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~float32 | ~float64
}
// Type with underlying int
type Point int
func Min[T Number](x, y T) T {
if x < y {
return x
}
return y
}
func main() {
// creating Point type
x, y := Point(5), Point(2)
fmt.Println(Min(x, y))
}
也可以不声明直接使用近似类型作为类型参数:
// Union operator and type approximation both use together without interface
func Min[T ~int | ~float32 | ~float64](x, y T) T {
if x < y {
return x
}
return y
}
约束也支持嵌套,Number约束嵌套使用Float约束:
// Float is made up of all the float type
type Float interface {
~float32 | ~float64
}
// Number is build from Integer and Float
type Number interface {
Integer | Float
}
// Using Number
func Min[T Number](x, y T) T {
if x < y {
return x
}
return y
}
## constraints 包
constraints包暴露一些预定义约束类型,既然已经内置了约束类型,最好直接使用。其中最重要的是[Ordered](https://pkg.go.dev/golang.org/x/exp/constraints#Ordered)约束,所有支持`>,<,==, !=` 操作的类型。
func min[T constraints.Ordered](x, y T) T {
if x > y {
return x
} else {
return y
}
}
## 总结
interface表示实现该interface的一组类型,泛型不是interface的替代,泛型是基于interface设计的,为了提升Go类型安全,同时消除代码重复。泛型是实际类型的占位符,在编译期间泛型代码可能会转换为基于interface的具体实现。
本文介绍了如定义类型参数,以及如何和函数和结构体一起使用类型参数实现泛型。同时也介绍了联合类型和近似类型,最后还提及了contraints包内置的类型。参考内容:https://blog.logrocket.com/understanding-generics-go-1-18/, 以及官方文档。