Go 泛型实战:用类型集合与 constraints 写出可复用的工具函数

为什么需要泛型

Go 1.18 之前,想要写“对任意类型都适用的函数”只有两条路:复制粘贴多份,或者用 interface{} 配合类型断言。前者臃肿难维护,后者在编译期丢失类型安全,运行时才可能 panic。

泛型(generics)用类型参数解决了这个矛盾:同一份逻辑,对多种类型都成立,同时保留编译期类型检查。

核心概念:类型集合与 ~T

泛型的基石是“类型集合”(type set)。约束(constraint)不再只是一个接口,而是一组允许的类型。最常见的写法是近似元素 ~T

// ~int 表示“底层类型是 int 的所有类型”,包括自定义类型 type MyInt int
type Number interface {
    ~int | ~int64 | ~float64
}

标准库 constraints 包提供了 Ordered(可比较大小)、IntegerFloat 等常用约束,comparable 则是语言内建的可比较约束。

实战一:泛型 Min/Max

constraints.Ordered 写出一个对任意可排序类型都成立的极值函数:

import "golang.org/x/exp/constraints"

func Min[T constraints.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func Max[T constraints.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

调用时类型可自动推导:Min(3, 5)Min("go", "rust") 都能编译通过,且返回类型与入参一致。

实战二:Slice 工具函数

泛型最实用的场景是通用容器操作。下面用三个函数实现 Map、Filter、Reduce:

func Map[T, U any](s []T, f func(T) U) []U {
    r := make([]U, len(s))
    for i, v := range s {
        r[i] = f(v)
    }
    return r
}

func Filter[T any](s []T, f func(T) bool) []T {
    var r []T
    for _, v := range s {
        if f(v) {
            r = append(r, v)
        }
    }
    return r
}

func Reduce[T, U any](s []T, init U, f func(U, T) U) U {
    acc := init
    for _, v := range s {
        acc = f(acc, v)
    }
    return acc
}

这三个函数对任意切片类型都适用,类型安全且零反射开销。例如把一组订单金额求和:

total := Reduce(orders, 0.0, func(acc float64, o Order) float64 {
    return acc + o.Amount
})

何时不该用泛型

泛型不是银弹。如果只有一两种类型用到,直接写具体类型更清晰;当逻辑差异很大、只是“看起来像”时,用接口 + 多态反而更合适。一个经验法则:

当同一份逻辑要作用于多种类型、且类型之间行为一致时,才上泛型;否则优先具体类型或接口。


小结

泛型让 Go 在保持简洁的同时获得了表达力:类型集合与 ~T 定义约束,constraints 包提供常用集合,Slice 工具函数是最高频的落地点。用对地方,代码既能复用又不失类型安全。