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())
}
相关推荐
心月狐的流火号4 小时前
Go语言错误处理
后端·go
lypzcgf1 天前
Coze源码分析-资源库-创建数据库-后端源码-应用/领域/数据访问层
数据库·go·后台·coze·coze源码分析·ai应用平台·agent平台
苏琢玉1 天前
作为 PHP 开发者,我第一次用 Go 写了个桌面应用
node.js·go·php
xuhe22 天前
Overleaf项目文件同步工具: olsync
linux·go·overleaf·sync
n8n2 天前
Go语言net/http库使用详解
go
n8n2 天前
Gin 框架令牌桶限流实战指南
go·gin
n8n2 天前
Gin框架整合Swagger生成接口文档完整指南
go
n8n2 天前
Go test 命令完整指南:从基础到高级用法
go
n8n2 天前
Go tool pprof 与 Gin 框架性能分析完整指南
go·gin