第二十三回:Go 语言泛型 (Generics)

万能锦盒的奥秘
唐伯虎
唐伯虎: 唉,送秋香姐礼物真是麻烦。送金钗要用“金钗盒”,送诗集要用“书画盒”,送鲜花要用“花篮”。
每次都要重新做一个盒子(定义新结构体),累死本才子了!有没有一种万能锦盒,既能装金钗,又能装诗集,还能保证类型安全呢?
秋香
秋香: 听说西域(Go 1.18+)传来一种叫泛型 (Generics) 的绝学,可以制作“模具”,想装什么就变什么!

📦 什么是泛型?

在泛型出现之前,我们只有两个选择:

  1. 写重复代码:为 int 写一个函数,为 string 写一个函数...(累死)
  2. 用 interface{}:什么都能装,但拿出来不知道是啥,容易出错(不安全)。

泛型允许你在定义函数或结构体时,先不指定具体类型,而是用一个占位符(通常叫 T)代替。等真正使用时,再告诉它 T 是什么。

🔧 打造万能工具 (泛型函数)

唐伯虎决定先打造一个“万能展示台”,可以展示任何东西。

package main
import "fmt"

// [T any] 是类型参数列表
// T 是占位符名字,随便起 (通常用 T, K, V)
// any 是约束,表示 T 可以是任何类型
func PrintGift[T any](gift T) {
    fmt.Printf("华安献上礼物: %v\n", gift)
}

func main() {
    // T 自动推导为 string
    PrintGift("《唐寅诗集》") 
    
    // T 自动推导为 int
    PrintGift(9527)
    
    // 显式指定 T 为 bool
    PrintGift[bool](true) 
}

📦 万能锦盒 (泛型结构体)

接下来是重头戏:定义一个能装万物的盒子结构体。

// 定义一个泛型结构体 Box
type Box[T any] struct {
    Name    string
    Content T // Content 的类型取决于 T
}

// 给泛型结构体添加方法
func (b *Box[T]) Open() {
    fmt.Printf("打开 %s,里面是: %v\n", b.Name, b.Content)
}

func main() {
    // 创建一个装字符串的盒子
    bookBox := Box[string]{
        Name:    "书画盒",
        Content: "百鸟朝凤图",
    }
    bookBox.Open()

    // 创建一个装整数的盒子
    moneyBox := Box[int]{
        Name:    "钱箱",
        Content: 10000,
    }
    moneyBox.Open()
}
石榴姐
石榴姐: 哎哟,那能不能给我做一个专门装“能比较大小的东西”的盒子?我要比比谁送我的钻石最大!

⚖️ 门当户对 (约束 Constraints)

有时候我们不能让 T 是任意类型 (any)。比如,我们想写一个比较大小的函数,T 就必须支持 >< 操作。

1. comparable (内置约束)

Go 内置了一个 comparable 约束,表示类型支持 ==!=

// 只有支持判等的类型才能传进来
func IsSameGift[T comparable](a, b T) bool {
    return a == b
}

func main() {
    fmt.Println(IsSameGift(100, 100))       // true
    fmt.Println(IsSameGift("香", "臭"))     // false
    // fmt.Println(IsSameGift([]int{1}, []int{1})) // ❌ 报错!切片不支持 == 比较
}

2. 自定义约束 (Interface with Union)

如果你想限制 T 只能是数字(支持加减乘除),可以自定义接口约束。

// 定义一个约束:只能是 int 或 float64
type Number interface {
    int | float64 | int64
}

// 使用自定义约束
func Add[T Number](a, b T) T {
    return a + b
}

func main() {
    fmt.Println(Add(1, 2))       // 3
    fmt.Println(Add(1.5, 2.5))   // 4.0
    // fmt.Println(Add("A", "B")) // ❌ 报错!string 不在 Number 约束里
}

🌊 只是像而已 (Type Approximation ~)

如果我定义了一个新类型 type Age int,它还能用上面的 Number 约束吗?

不能!除非你在约束里加个波浪号 ~

type MyInt int

// ~int 表示:所有底层类型是 int 的类型都能用
type Integer interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

func Max[T Integer](a, b T) T {
    if a > b {
        return a
    }
    return b
}

func main() {
    var a, b MyInt = 10, 20
    fmt.Println(Max(a, b)) // ✅ 可以用了!
}

🎯 练功房(通用的切片反转)

唐伯虎想把一串珠子(切片)倒过来串。请写一个泛型函数 Reverse,能反转任何类型的切片。

package main
import "fmt"

// 填空:定义泛型函数
func Reverse[T ____](s []T) []T {
    l := len(s)
    result := make([]T, l)
    for i, v := range s {
        result[l-1-i] = v
    }
    return result
}

func main() {
    nums := []int{1, 2, 3}
    fmt.Println(Reverse(nums)) // [3 2 1]

    strs := []string{"秋", "香", "姐"}
    fmt.Println(Reverse(strs)) // [姐 香 秋]
}

任务: 填空,使函数能接收任何类型。

答案: any

func Reverse[T any](s []T) []T { ... }

解析: 反转操作不需要比较,也不需要计算,只需要移动位置,所以 any 类型就足够了。