Go语言一日一学:深入理解Generics(泛型)

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] 定义了一个类型参数 Tany 是 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())
}
相关推荐
梁梁梁梁较瘦1 天前
边界检查消除(BCE,Bound Check Elimination)
go
梁梁梁梁较瘦1 天前
指针
go
梁梁梁梁较瘦1 天前
内存申请
go
半枫荷1 天前
七、Go语法基础(数组和切片)
go
梁梁梁梁较瘦2 天前
Go工具链
go
半枫荷2 天前
六、Go语法基础(条件控制和循环控制)
go
半枫荷3 天前
五、Go语法基础(输入和输出)
go
小王在努力看博客3 天前
CMS配合闲时同步队列,这……
go
Anthony_49264 天前
逻辑清晰地梳理Golang Context
后端·go
Dobby_055 天前
【Go】C++ 转 Go 第(二)天:变量、常量、函数与init函数
vscode·golang·go