Go语言一日一学:深入理解Generics(泛型)
Go 语言在 1.18 版本引入了泛型(Generics),这是 Go 语言发展史上的一个重要里程碑。泛型使得开发者能够编写更灵活、可重用且类型安全的函数和数据结构,避免了在处理不同类型但逻辑相同的代码时进行大量的重复劳动。今天,我们就来深入学习 Go 语言的泛型。
为什么需要泛型?
在泛型出现之前,如果你想编写一个可以处理多种类型数据的函数,比如一个简单的查找最小值函数,你可能需要为每种类型都写一个独立的函数:
go
func MinInt(a, b int) int {
if a < b {
return a
}
return b
}
func MinFloat64(a, b float64) float64 {
if a < b {
return a
}
return b
}
或者,你可能使用 interface{}
和类型断言,但这会牺牲类型安全性,并且在运行时会有额外的开销:
go
func MinInterface(a, b interface{}) interface{} {
// 需要类型断言,并且在编译时无法保证类型正确性
// ...
return nil
}
泛型的出现,彻底解决了这种代码重复和类型不安全的问题。
Go 泛型的核心概念
Go 泛型的引入主要围绕两个核心概念:类型参数(Type Parameters)和类型约束(Type Constraints)。
1. 类型参数 (Type Parameters)
类型参数允许你定义一个函数或类型,使其在操作时可以使用一个或多个未知类型。这些未知类型会在函数或类型被调用或实例化时确定。
在函数或类型名称后,使用方括号 []
来声明类型参数。
函数示例:
go
// T 是类型参数,它代表一个未知的类型
func PrintSliceT any [<sup>1</sup>](s []T) {
for _, v := range s {
fmt.Println(v)
}
}
在这个例子中,[T any]
定义了一个类型参数 T
。any
是 Go 语言内置的类型约束,表示 T
可以是任何类型。
类型示例:
go
type MyGenericSlice[T any] []T
这里 MyGenericSlice
成为了一个泛型类型,可以存储任何类型的切片。
2. 类型约束 (Type Constraints)
类型约束是泛型的关键所在。它限制了类型参数可以代表的类型,确保在泛型代码内部对这些类型执行的操作是合法的。
类型约束实际上是接口类型。这意味着你可以在接口中定义泛型类型所需的方法。
示例:constraints.Ordered
Go 标准库的 golang.org/x/exp/constraints
包提供了一些常用的类型约束,其中 Ordered
接口就是非常典型的一个。它包含了所有支持比较运算符(<, <=, >, >=)的基本类型。
go
import "golang.org/x/exp/constraints"
// MinFunction 是一个泛型函数,可以计算任何实现了 constraints.Ordered 接口的类型的最小值
func MinFunctionT constraints.Ordered [<sup>2</sup>](a, b T) T {
if a < b {
return a
}
return b
}
现在,MinFunction
可以用于 int
, float64
, string
等所有实现了 constraints.Ordered
接口的类型,而不需要为每种类型单独编写函数。
使用泛型:gobyexample.com/generics
解析
我们通过 gobyexample.com/generics
的例子来进一步理解和实践泛型。
go
package main
import "fmt"
// Go 的泛型允许你编写在不同类型上工作的函数和数据结构。
// 这里的 "MapKeys" 函数有两个类型参数 - K 和 V。
// K 必须是可比较的,因为它被用作 map 的键。
// V 可以是任何类型 ([any] 约束)。
// 这个函数返回一个 []K 类型的值 - 一个由 map 的 K 类型的键组成的切片。
func MapKeysK comparable, V any [<sup>3</sup>](m map[K]V) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}
// 第二个例子是我们之前提到的 Min 函数。
// 这里的类型约束 [constraints.Ordered] 意味着我们传入的类型
// 必须支持有序比较(<,> 等)。
// 这允许我们对任意有序类型(如 int、float64、string)计算最小值。
type List[T any] struct {
head, tail *element[T]
}
type element[T any] struct {
next *element[T]
val T
}
func (lst *List[T]) Push(v T) {
if lst.tail == nil {
lst.head = &element[T]{val: v}
lst.tail = lst.head
} else {
lst.tail.next = &element[T]{val: v}
lst.tail = lst.tail.next
}
}
func (lst *List[T]) GetAll() []T {
var elems []T
for e := lst.head; e != nil; e = e.next {
elems = append(elems, e.val)
}
return elems
}
func main() {
var m = map[int]string{1: "2", 2: "4"}
fmt.Println("keys:", MapKeys(m))
_ = MapKeysint, string [<sup>4</sup>](m)
// 当使用泛型函数时,你通常不需要显式指定类型参数。
// 编译器通常可以推断出你想要使用的类型参数。
// 例如,尽管 MapKeys 接受 K 和 V,我们只需要传入 m。
//
// map[string]int 中的键是 string,值是 int。
m2 := map[string]int{"hello": 1, "world": 2}
fmt.Println("keys:", MapKeys(m2))
// 泛型也可以用在自定义类型上。
// 定义一个链表,可以包含任意类型 T 的值。
lst := List[int]{}
lst.Push(10)
lst.Push(20)
lst.Push(30)
fmt.Println("list:", lst.GetAll())
}