Go语言泛型使用场景

2022-02-13 ⏳1.9分钟(0.8千字) g

我已经写过多篇介绍泛型的文章。但跟所有技术一样,泛型也有其适用的范围。今天就讨论一下什么时候应该使用泛型,什么时候又不该使用它。

先说应该使用泛型的场景。

合适的场景

函数式编程

第一个场景就是函数式编程。如果你的函数是操作 slice/map/chan 等对象,但不关心每个元素的类型,那就很适合使用泛型。最典型的就是过滤 slice 中的元素,提取 map 的 key 或者 value,合并不同的 channel 等等。这在我的泛型示例一文中都有具体的示例代码。

通用数据结构

第二个场景就是通用数据结构。比如链表、堆、树、图等结构。如果没有泛型,就只能将参数的类型定为interface{},这样就无法利用编译器来检查类型错误。

比如现在Go语言的container/list包,其Element定义如下:

type Element struct {
  Value interface{}
}

如果有了泛型,那就可以改为:

type Element[T any] struct {
  Value T
}

从而支持各种类型的数据结构。

在使用泛型的时候,优先使用泛型函数而非泛型成员方法。比如下面的二叉树定义:

type Tree[T any] struct {
  cmp func(T, T) int
  root *leaf[T]
}

type leaf[T any] struct {
  val T,
  left, right *leaf[T]
}

func NewTree[T any](vars []T, cmp func(T, T) bool) *Tree[T] {
  // ...
}

Tree中的类型参数T使用any约束,比较方法cmp需要单独指定。这样使用起来会更加方便,比如针对int类型,我们可以:

t := NewTree([]int{1,2,3,4}, func(a, b int) { return a < b })

但如果我们将T限定为interface{cmp(T, T) bool},那么,我们就不能简单地初始化一个int类型的二叉树,因为int类型没有实现cmp方法。为此,我们只能自定义一个特殊的int类型,然后给它加cmp成品方法才能再构建二叉树。

大量相似逻辑

第三个场景就是所有成员函数的逻辑非常相似。

以排序为例,sort包要求被排序的列表实现sort.Interface接口。通常,列表都使用切片表示,而对应的sort.Interface实现也都非常相似:

type ints []int

type (is ints) Len() int { return len(is) }
type (is ints) Swap(i, j int) { is[i],is[j] = is[j],is[i] }
type (is ints) Less(i, j int) bool { ... }

对于任意类型的切片,Len和Swap方法完全一样,而 Less 方法也只需提供不同的比较逻辑即可。这种场景就特别适合使用泛型来简化代码:

type sliceFn[T any] struct {
  s   []T
  cmp func(T, T) bool
}

func (s sliceFn[T]) Len() int           { return len(s.s) }
func (s sliceFn[T]) Less(i, j int) bool { return s.cmp(s.s[i], s.s[j]) }
func (s sliceFn[T]) Swap(i, j int)      { s.s[i], s.s[j] = s.s[j], s.s[i] }

func SliceFn[T any](s []T, cmp func(T, T) bool) {
	sort.Sort(sliceFn[T]{s, cmp})
}

使用方法如下:

sort.SliceFn([]int{1,2,3}, func(a, b int) bool { return a < b})
sort.SliceFn([]string{"a","b","c"}, func(a, b string) bool { return a < b})

以上是适合使用泛型的场景。下面讲一下不适合的场景。

不合适的场景

只调函数不返回类型

目前只有一个场景,就是只调参数的一个方法,比如:

func ReadFour[T io.Reader](r T) (buf []byte, err error) {
  buf = make([]byte, 4)
  n, err = r.Read(buf)
  // ...
}

这一类情况完全不需要使用泛型,应该使用接口:

func ReadFour(r io.Reader) (buf []byte, err error) {
  // ...
}

总结

如果使用泛型会让代码变得更复杂,那也应该避免。大家可以尝试我在前传一文中提到的替代方法。总之,大家在使用的时候要充分要考虑泛型的使用场景,不要一把梭。

参考链接: