入门Go泛型两种场景 | 青训营

61 阅读2分钟

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
}