泛型编写教程和建议
1 背景
从开源社区了解,有许多要求在go中添加泛型的声音,因此在1.18中添加了此功能,本文主要翻译和改自此文,感兴趣的同学可以看原文,文章写的更加详细和透彻。本文也会介绍go泛型的高级用法,适用于已经熟悉泛型在Go语言中工作的人。
2 设计
后面将会举例简单的示例来描述泛型设计。
3 泛型使用
3.1 类型参数
通用代码使用到的参数称之为通用类型参数。当使用泛型时将会替代所使用的类型参数。下面举一个简单例子,打印所有切片元素:
func Print(s []T) { // 只是一个例子,不是建议的语法。
for _, v := range s {
fmt.Println(v)
}
}
使用这个方法时,应该如何声明T这个类型参数呢?因此go中引入了[T any]这种写法代表泛型的类型:
func Print[T any](s []T) {
for _, v := range s {
fmt.Println(v)
}
}
根据这样整理,就可以打印出任何符合该泛型类型的切片了!
3.2 约束
下面呢,我们将例子变的稍微复杂一点,让我们把它变成一个函数,通过在每个元素上调用String()方法,得到结果转为切片返回。
// 这个函数是无效的。
func Stringify[T any](s []T) (ret []string) {
for _, v := range s {
ret = append(ret, v.String()) // 无效
}
return ret
}
我们乍一看这样的写法是没错的,但是T属于any类型,也就是任意类型。并不是所有类型都有String()方法,这里需要注意这一点,因此我们需要将类型进行约束才可以使该函数正确运行。
3.2.1 定义约束
go中的接口这个结构其实可以完全满足对于类型的约束,例如下Stringfy接口:
type Stringer interface{
String() string
}
3.2.2 使用约束
对于泛型函数,可以将约束视为类型参数。因此可以成作如下:
// Stringify 对 s 的每个元素调用 String 方法,
// 并返回结果。
func Stringify[T Stringer](s []T) (ret []string) {
for _, v := range s {
ret = append(ret, v.String()) // 可以运作
}
return ret
}
所以上例中的v.String()是可以正常运作的,因为我们将此接口作为其参数约束。
3.4 多种类型参数
上面的例子只是简单的单个参数,扩展一下就可以得到多种类型参数的方式,如下:
// Print2 有两个any类型的两个不同参数。
func Print2[T1, T2 any](s1 []T1, s2 []T2) { ... }
以及
// Print2Same 有一个any类型的两个不同参数。
func Print2Same[T any](s1 []T, s2 []T) { ... }
这样就可以让泛型有了更好的扩展性,满足我们日常开发更多的需要。
3.5 通用类型
除了泛型函数,我们还需要支持泛型类型,这里建议扩展类型来获取类型参数:
type Vector[T any] []T // 声明泛型类型参数
func (v *Vector[T]) Push(x T) { // 这里不需要声明约束,因为上述定义时已经约束,这里只需要使用该类型即可
*v = append(*v, x)
}
func main() {
var v Vector[int]
v.Push(1)
fmt.Println(v)
}
泛型同样适用于struct类型中,但是需要注意一点的是,如果自身引用泛型,必须按照定义的顺序进行,否则可能出现无限递归的问题:
// List 是一个 T 类型值的链表
type List[T any] struct {
next *List[T] // 这个对 List[T] 的引用是 OK
val T
}
// 这个类型是无效的。
type P[T1, T2 any] struct {
F *P[T2, T1] // 无效;必须是 [T1, T2] ,否则实例化可能出现问题
}
3.6 操作符
正如我们所见,我们使用接口类型作为约束,接口类型仅仅提供了一组方法而已。然而泛型方法不能表达我们需要的一切,如下我们需要返回切片中最小的元素:
// 这个函数是无效的。
func Smallest[T any](s []T) T {
r := s[0] // 如果切片为空
for _, v := range s[1:] {
if v < r { // INVALID
r = v
}
}
return r
}
上面例子的写法是不成立的,因为T是any类型,并不能通过操作符进行比较大小。因此我们需要定义约束集合来限制T的类型符合我们需要。这里必须要有序比较才可以,comparable是不可行的,它只用于==和!=的比较。这里可以使用的类型有int、float等ordered类型,因此我们可以将上述修改如下:
// 这个函数是无效的。
func Smallest[T interface{int|int8|~float}](s []T) T {
r := s[0] // 如果切片为空
for _, v := range s[1:] {
if v < r {
r = v
}
}
return r
}
这样的写法可以满足我们要求。
我们还可以定义一个约束类型集合来拱我们自己使用,具体例子如下:
type PredeclaredSignedInteger interface {
int | int8 | int16 | int32 | int64
}
3.7 约束中的可比较类型
前面我们提到,运算符只能用于语言预先声明的类型的规则有两个例外。例外是==and !=,它们允许用于结构、数组和接口类型。这些足够有用,我们希望能够编写一个接受任何可比较类型的约束。
// Index 返回 x 在 s 中的索引,如果没有找到则返回 -1。
func Index[T comparable](s []T, x T) int {
for i, v := range s {
// v 和 x 是类型 T,具有可比较
// 约束,所以我们可以在这里使用 == .
if v == x {
return i
}
}
return -1
}
由于comparable是一个约束类型,因此可以嵌入到接口中
type ComparableHaser interface{
comparable
Hash() unitptr
}
ComparableHaser可以通过Hash函数得到的值来进行比较是否相等来判断两个类型是否相等。
3.8 相互引用类型参数
类型之间相互引用在开发过程中可能会出现,例如获取连接A点的所有边或者获取A边的两端点,这就可能需要相互引用的参数,如下为示例:
type NodeConstraint[Edge any] interface {
Edges() []Edge
}
type EdgeConstraint[Node any] interface {
Nodes() (from, to Node)
}
// 图是由节点和边组成的图。两者相互引用
type Graph[Node NodeConstraint[Edge], Edge EdgeConstraint[Node]] struct {
}
type GNode struct{}
type GEdge struct{}
func (g *GNode) Edges() []*GEdge {
return nil
}
func (g *GEdge) Nodes() (from, to *GNode) {
return &GNode{}, &GNode{}
}
func New[Node NodeConstraint[Edge], Edge EdgeConstraint[Node]](nodes []Node) *Graph[Node, Edge] {
fmt.Println(nodes)
return nil
}
func main() {
New[*GNode, *GEdge]([]*GNode{})//调用相互引用类型时,必须声明两个类型显示调用,这里编译器无法自动推断
}
3.9 类型推断
在许多情况下,我们可以使用类型推断来避免显示的写出类型参数。并且可以根据已知的类型参数推断出未知的类型参数。例如:
func Map[F, T any](s []F, f func(F) T) []T { ... }
可以通过下面的方式调用。
var s []int
f := func(i int) int64 { return int64(i) }
var r []int64
// Specify both type arguments explicitly.
r = Map[int, int64](s, f)
// Specify just the first type argument, for F,
// and let T be inferred.
r = Map[int](s, f)
// Don't specify any type arguments, and let both be inferred.
r = Map(s, f)
如果在没有指定所有类型参数的情况下使用泛型函数或者类型,那么就无法推断出类型,则会出现错误。