Go 1.18版本增加了一个主要的新语言特性: 对泛型的支持。在本文中,我不会描述泛型是什么,也不会描述如何使用它们。本文讨论在 Go 代码中何时使用泛型,以及何时不使用它们。 事先声明,本文提供的是一般指导建议,而不是硬性规定。是否使用泛型需要你自己的判断。但如果你不确定,那么建议使用这里讨论的指南。
使用容器类型
当我们编写的是操作 Go 语言定义的特殊容器类型(slice、map和chennel)的函数。如果函数具有包含这些类型的参数,并且函数的代码并不关心元素的类型,那么使用类型参数可能是有用的。 例如,这里有一个函数,它的功能是返回任何类型map中所有的key:
func MapKeys[Key comparable, Val any](m map[Key]Val) []Key {
s := make([]Key, 0, len(m))
for k := range m {
s = append(s, k)
}
return s
}
这段代码并不关注 map 中键的类型,也根本没有使用 map 值类型。它适用于任何map类型。这是使用类型参数的一个很好示例。 在引入类型参数之前,想要实现类似功能通常是使用反射,但是使用反射实现通常是复杂的,并且在编译期间不会进行静态类型检查,在运行时通常速度也更慢。
通用数据结构
类型参数另一个适用场景就是用于通用数据结构。通用数据结构类似于slice或map,但不是内置在语言中的,例如链表或二叉树。 之前需要这种数据结构的程序通常采用下面两种方法中的一个:使用特定的元素类型编写数据结构,或者使用接口类型。用类型参数替换特定的元素类型可以生成更通用的数据结构,该数据结构可以在程序的其他部分或其他程序中使用。用类型参数替换接口类型可以更有效地存储数据,节省内存资源;它还允许代码避免类型断言,并在构建时进行完全的类型检查。 例如,下面是使用类型参数的二叉树数据结构的一部分:
// Tree is a binary tree.
type Tree[T any] struct {
cmp func(T, T) int
root *node[T]
}
// A node in a Tree.
type node[T any] struct {
left, right *node[T]
val T
}
// find returns a pointer to the node containing val,
// or, if val is not present, a pointer to where it
// would be placed if added.
func (bt *Tree[T]) find(val T) **node[T] {
pl := &bt.root
for *pl != nil {
switch cmp := bt.cmp(val, (*pl).val); {
case cmp < 0:
pl = &(*pl).left
case cmp > 0:
pl = &(*pl).right
default:
return pl
}
}
return pl
}
// Insert inserts val into bt if not already there,
// and reports whether it was inserted.
func (bt *Tree[T]) Insert(val T) bool {
pl := bt.find(val)
if *pl != nil {
return false
}
*pl = &node[T]{val: val}
return true
}