Skip to content
Go
泛型详解 (Go 1.18+)

泛型详解 (Go 1.18+)

Go 1.18 引入了泛型(类型参数),这是 Go 语言历史上最大的语言变更。泛型让你可以编写类型安全且可复用的代码,而不需要牺牲类型安全去使用 interface{}。

Go 1.27 又补上了最后一块大短板:泛型方法(方法可以声明自己的类型参数)。本篇按"语法 → 约束 → 推断 → 方法限制 → 标准库应用 → 底层实现 → 陷阱"的顺序展开。


1. 为什么需要泛型?

在没有泛型的时代,你有两种选择:

// 方案 A:为每种类型写一份代码(类型安全,但重复)
func MaxInt(a, b int) int       { if a > b { return a }; return b }
func MaxFloat(a, b float64) float64 { if a > b { return a }; return b }

// 方案 B:用 interface{}(通用,但失去类型安全 + 装箱/断言开销)
func MaxAny(a, b any) any { /* 调用方还得自己断言回来 */ }

泛型给了你第三种选择:写一次代码,适用于多种类型,保持类型安全。标准库的 slices/maps/cmp 正是靠它才可能在保持零接口污染的前提下收编散落的工具函数。


2. 基础语法

2.1 泛型函数

// 语法:func 函数名[类型参数 约束](参数列表) 返回值
func Max[T cmp.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

// 调用
x := Max[int](3, 5)        // 显式指定类型参数
y := Max(3.14, 2.71)       // 类型推断:自动推断为 float64
z := Max("hello", "world") // 类型推断:自动推断为 string

提示:需要"可比较大小"时用标准库的 cmp.Ordered(Go 1.21+),不要再用早期资料里的 golang.org/x/exp/constraints.Ordered——那是实验包,stdlib 转正后已无必要。

2.2 泛型类型

type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T // 泛型里拿零值的标准写法
        return zero, false
    }
    n := len(s.items) - 1
    item := s.items[n]
    s.items = s.items[:n]
    return item, true
}

st := Stack[int]{}
st.Push(42)
v, _ := st.Pop() // v 是 int,值为 42

Pop 里那个 var zero T 是惯用法:你不能对类型参数写 return nil(T 不一定是指针/接口),也不能 return T()(不能把类型名当函数调用)。

2.3 类型参数命名规范

// 约定:
// T        — 单一类型参数(最常用)
// T, U, V  — 多个类型参数
// K, V     — 键值对(map 的 key / value)
// E        — 元素类型(slice 的元素)
// S        — slice 类型本身

func Keys[K comparable, V any](m map[K]V) []K { ... }
func Contains[E comparable](s []E, v E) bool { ... }

3. 类型约束 (Type Constraints)

约束定义了类型参数必须满足的条件,是泛型系统的核心。

3.1 any — 无约束

// any 是 interface{} 的别名(Go 1.18 引入),接受任何类型
func Print[T any](v T) {
    fmt.Println(v)
}

3.2 comparable — 可比较类型

// comparable 是预声明约束:可以用 == 和 != 比较的类型
// 不包括 slice、map、function(它们的 == 不合法)
func Equal[T comparable](a, b T) bool {
    return a == b
}

Equal(1, 2)       // true
Equal("hi", "hi") // true
// Equal([]int{}, []int{}) // 编译错误:slice 不满足 comparable

两个容易搞混的点:

  1. comparable 只保证 ==/!=,想要 >/< 得用 cmp.Ordered。
  2. 约束里写了 comparable,不代表任何 comparable 类型都行——接口类型(如 error)在静态上满足 comparable,但如果 T 实际装的是 slice/map,运行时会 panic。更隐蔽的是 Go 1.20+ 规范调整过:comparable 约束现在要求"所有可能实例化出的类型都支持 == “,直接传 []int 给 Equal[[]int] 依然编译不过,但把 slice 装进接口再实例化仍可能运行时炸。

3.3 自定义约束 — 接口即约束

// 任何实现了 fmt.Stringer 的类型都可以作为 T
func Stringify[T fmt.Stringer](v T) string {
    return v.String()
}

type User struct{ Name string }

func (u User) String() string { return u.Name }

s := Stringify(User{"Alice"}) // 合法,推断 T = User

3.4 联合类型约束 (Union)

// 用 | 列举允许的具体类型
type Number interface {
    int | int32 | int64 | float32 | float64
}

func Add[T Number](a, b T) T {
    return a + b
}

Add(1, 2)     // 合法 int
Add(1.5, 2.3) // 合法 float64
// Add("a", "b") // 编译错误:string 不在 Number 中

联合约束里所有成员支持的公共操作才可用——int | float64 能写 +,但 int | string 的 + 语义不同(数字加 vs 字符串拼接)却同样合法(两者都支持 +),想要"只有数字"就老老实实列数字类型。

3.5 ~ — 近似约束

type MyInt int   // 底层类型是 int
type Celsius float64

// ~T 表示"底层类型为 T"的所有类型(包括自定义类型)
type Flexible interface {
    ~int | ~float64
}

func Double[T Flexible](v T) T {
    return v * 2
}

Double(MyInt(5))     // 合法:MyInt 底层是 int
Double(Celsius(3))   // 合法:Celsius 底层是 float64
Double(3.14)         // 合法:float64
// Double[string]("x") // 编译错误

不写 ~ 就只接受字面类型本身(int 约束只收 int,不收 MyInt)。给库设计"接受任何数值"的 API 时,~ 几乎是必选项。

3.6 内联约束

// 约束可以直接写在签名里,不必先声明接口
func Add[T int | float64](a, b T) T {
    return a + b
}

func Scale[T ~int | ~float64](v T, factor float64) T {
    return T(float64(v) * factor)
}

内联约束不能包含方法——要约束方法集,必须定义成命名接口。

3.7 约束的组合

// 接口可以嵌入 comparable/其他接口 + 方法
type ComparableHasher[T comparable] interface {
    comparable
    Hash() uint64
}

// 类型联合和方法不能混在同一个接口里
// type Bad interface { int | string; String() string } // 不允许

4. 类型推断

编译器会从实参反推类型参数,但有边界。

func First[T any](s []T) T { return s[0] }

names := []string{"Alice", "Bob"}
first := First(names) // 推断 T = string

推断不出、必须显式写的场景:

// 1) 类型参数只出现在返回值里
func New[T any]() *T {
    var v T
    return &v
}
// p := New()      // 推不出
p := New[int]()    // 必须显式

// 2) 泛型方法目前仍需显式指定的情况——见 5.2

Go 1.21 增强了一轮推断(可以从约束里的类型列表反推),Go 1.27 又把"函数赋给函数类型 / 类型转换"场景的推断推广到所有位置——实践中表现为:以前很多要写 Foo[T](x) 的地方现在能省。但别指望跨不同 Go 版本行为完全一致,升级工具链后偶发的"以前能编、现在报错"多与此有关。


5. 泛型与方法

5.1 泛型类型的方法

方法可以使用所属类型的类型参数,这是 1.18 起就合法的:

type Set[T comparable] struct {
    data map[T]struct{}
}

func NewSet[T comparable](vals ...T) *Set[T] {
    s := &Set[T]{data: make(map[T]struct{})}
    for _, v := range vals {
        s.data[v] = struct{}{}
    }
    return s
}

func (s *Set[T]) Add(v T)      { s.data[v] = struct{}{} }
func (s *Set[T]) Contains(v T) bool { _, ok := s.data[v]; return ok }
func (s *Set[T]) ToSlice() []T {
    out := make([]T, 0, len(s.data))
    for k := range s.data {
        out = append(out, k)
    }
    return out
}

5.2 Go 1.27:方法可以声明自己的类型参数

1.18~1.26 最大的抱怨就是"方法不能是泛型的”——Set[T].Map[U] 这种从 T 映射到 U 的操作只能写成包级函数。Go 1.27 正式支持了泛型方法(go.mod 的 go 指令需 ≥ 1.27,否则 go vet 的 stdversion/编译器会拦住)。

// Go 1.27:方法自带类型参数 U,合法
func (s *Set[T]) Map[U any](fn func(T) U) []U {
    out := make([]U, 0, len(s.data))
    for k := range s.data {
        out = append(out, fn(k))
    }
    return out
}

strs := s.Map(func(i int) string { return "#" + strconv.Itoa(i) }) // U 可推断
strs2 := s.Map[string](func(i int) string { return "#" + strconv.Itoa(i) }) // 显式也行

标准库自己就是例子:math/rand/v2 的 Rand 在 1.27 多了泛型方法 N[Int intType](n Int) Int,与包级函数 N 同语义。

四条边界(本地 go1.27 编译实测):

// 1. 接口方法仍然不能声明类型参数
//    type MyIface interface { Map[U any](f func(int) U) []U }
//    → 编译错误:interface method must have no type parameters

// 2. 泛型方法不能用于实现接口
//    type I interface{ Do(x int) int }
//    func (s *S) Do[T any](x T) T { ... }
//    var _ I = (*S)(nil)
//    → 编译错误:cannot use (*S)(nil) ... (method Do is missing)

// 3. 方法的类型参数不能遮蔽接收者已有的类型参数
//    func (b *Box[T]) Do[T any](x T) T { ... }
//    → 编译错误:T redeclared in this block

// 4. 类型参数也不能在方法签名里重复声明
//    func (s *Set[T]) Pick[T comparable](...) → 同样报 redeclared

设计意图很清晰:泛型方法只是"放在类型命名空间里的泛型函数",不进入任何接口/动态派发体系。所以:

  • 需要链式 Builder、Pipeline 转换(p.Map(f).Filter(g))——1.27 可以优雅实现。
  • 想让"泛型方法"可被 mock、可存进 []Speaker——不行,那是接口的活。
  • 给 *http.Request 加 JSONBody[T]() 这种"给别人的类型加泛型方法"依然做不到——Go 不允许给外部类型定义方法,与泛型无关。
// 1.27 之前被迫的写法(仍然合法,迁移期两者并存)
func MapSet[T comparable, U any](s *Set[T], fn func(T) U) []U { ... }

// 1.27 之后
func (s *Set[T]) Map[U any](fn func(T) U) []U { ... }

迁移注意:泛型方法要求 go.mod 里 go 1.27。库作者如果还要支持 1.26 及以下,应继续用包级函数,或在文件上用 //go:build go1.27 隔离。


6. 泛型 vs 接口

6.1 怎么选

// 接口:运行时多态,装箱 + 方法表动态派发
func StringifyIface(v fmt.Stringer) string { return v.String() }

// 泛型:按类型特化派发,可直接内联
func StringifyGen[T fmt.Stringer](v T) string { return v.String() }
特性 接口 泛型
多态方式 运行时动态派发 编译期确定具体类型
能否混装不同类型 能([]Animal{Dog{}, Cat{}}) 不能(元素类型唯一)
能否 mock/替换实现 能 调用点写死,无法注入
典型用途 行为抽象、依赖倒置 容器、算法、同构数据

经验法则:你在写"对同类型元素的集合做操作"(容器/算法/序列化壳),用泛型;你在表达"某类东西都会做某事"(行为),用接口。两者常配合:func Sort[T any](s []T, less func(T, T) bool)——约束保持 any(最小化),比较行为用函数注入而不是塞进接口。

6.2 约束最小化原则

// 好:只需要 ==,就只约束 comparable
func Contains[E comparable](s []E, v E) bool {
    for _, e := range s {
        if e == v {
            return true
        }
    }
    return false
}

// 差:明明只需要 == 却约束成 cmp.Ordered,把 float64/自定义 string 类型拒之门外

约束每收紧一分,调用方可选的类型就少一分;将来放宽约束是兼容性变更,收紧不是——一开始就写到刚好够用。


7. 标准库里的泛型

Go 1.21 起标准库开始大规模使用泛型,理解这些 API 的演化能帮你少踩"新旧写法混用"的坑。

7.1 slices 包 (Go 1.21+)

import "slices"

// 查找
slices.Contains([]int{1, 2, 3}, 2)      // true
slices.Index([]string{"a", "b"}, "b")   // 1
slices.Equal(a, b)                       // 元素逐个 ==

// 排序
slices.Sort(nums)                        // 要求 cmp.Ordered
slices.SortFunc(users, func(a, b User) int {
    return cmp.Compare(a.Age, b.Age)     // 返回负/零/正
})
slices.SortStableFunc(...)

// 去重/整理(注意:Compact 只去"相邻"重复,先排序再去重)
slices.Compact([]int{1, 1, 2, 3, 3})    // [1 2 3]
slices.Clip(nums)                        // 丢弃多余容量

// 插入与删除(都是原地移动,会修改底层数组)
nums = slices.Insert(nums, 2, 99)
nums = slices.Delete(nums, 1, 3)         // 删除 [1,3) 区间

// 版本差异
slices.Concat(a, b)   // Go 1.22
slices.Repeat(s, n)   // Go 1.23(不是 1.22)

陷阱:slices.Delete/Compact/CompactFunc 返回的是原底层的切片视图,被删元素会残留在尾部且不置零。如果原 slice 还有别的别名指向同一段内存,或者存的是指针且你希望 GC 回收,删完要么 clear(s[len:n]) 置零,要么自己拷贝。

7.2 maps 包 (Go 1.21+)

import "maps"

m := map[string]int{"a": 1, "b": 2}

m2 := maps.Clone(m)             // 浅拷贝(键值直接复制)
maps.Copy(m2, m3)               // 把 m3 覆盖进 m2
maps.DeleteFunc(m, func(k string, v int) bool { return v < 2 })
maps.Equal(m, m2)               // 要求 V comparable

// Go 1.23 起 Keys/Values 返回迭代器(不是切片!)
keys := slices.Sorted(maps.Keys(m))   // 惯用法:配 slices.Sorted

版本坑:Go 1.23 之前 maps.Keys(m) 返回 []K,1.23 起返回 iter.Seq[K]——升级 1.23+ 时所有 ks := maps.Keys(m) 再直接 sort.Strings(ks) 的代码都会编译失败,必须改成 slices.Sorted(maps.Keys(m))。maps.Values 同理。

7.3 cmp 包 (Go 1.21+)

import "cmp"

cmp.Compare(1, 2)     // -1
cmp.Compare(2, 1)     // 1
cmp.Compare("a", "b") // -1
cmp.Or(cfg.Host, "localhost")   // Go 1.22:返回第一个非零值
var _ cmp.Ordered              // int/float/string 及其 ~自定义类型
cmp.Less / cmp.Compare 对结构体用 cmp.Equal 递归比较(测试/深层判等利器)

cmp.Equal 和 reflect.DeepEqual 的差别值得记:cmp.Equal 遵循 Equal 方法(如 time.Time.Equal),而 DeepEqual 逐字段比。比较时间、netip.Addr 这类"有语义相等"的值时用 cmp。

7.4 sync/atomic 与 sync 的泛型成员

var counter atomic.Int64
var ready   atomic.Bool
var ptr     atomic.Pointer[MyStruct] // Go 1.19+

// Go 1.21:一次性初始化三件套
var initDB    = sync.OnceFunc(func() { db = connectDB() })
var getConf   = sync.OnceValue(func() *Config { return loadConfig() })
var fetchData = sync.OnceValues(func() ([]Item, error) { return fetchItems() })

OnceValue/OnceValues 把"懒加载 + 只成功一次"写成了表达式级的东西,比手搓 sync.Once + 闭包捕获变量干净得多。

7.5 iter 包与 range-over-func (Go 1.23+)

Go 1.23 把"用 for range 遍历自定义逻辑"变成语言能力,配套 iter 包只有四个类型:Seq[V]、Seq2[K,V]、Pull[V]、Pull2[K,V]。

Seq 与 yield

type Seq[V any]  func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)

yield 是编译器在 for range 时生成的回调:产出当前值,并返回"消费者是否还想继续"。break/return 会让 yield 返回 false,迭代器必须立刻停止。

countdown := func(yield func(int) bool) {
    for i := 3; i > 0; i-- {
        if !yield(i) { // 消费者 break → 马上退出
            return
        }
    }
}

for v := range countdown { // 合法:countdown 的签名匹配 iter.Seq[int]
    fmt.Println(v)         // 3 2 1
    if v == 2 { break }    // yield 返回 false,countdown 随之返回
}

设计取舍值得理解:push 模型(迭代器驱动循环体)让 defer 和资源清理天然正确,也避免了 pull 模型里"暂停/恢复状态机"的复杂性;代价是你不能"手动要下一个值",交错遍历得靠 iter.Pull 桥接。

iter.Pull:从推转拉

seq := slices.Values([]int{10, 20, 30})
next, stop := iter.Pull(seq)
defer stop() // 必须调用——Pull 内部起 goroutine,不调就泄漏

for {
    v, ok := next()
    if !ok { break }
    fmt.Println(v)
}

性能:iter.Pull 内部用 goroutine + channel 做状态切换,比直接 for range 贵好几个数量级。只有"必须交错/惰性控制步进"时再用它。

标准库的迭代器生产与消费

// 生产
slices.All(s) / slices.Values(s) / slices.Backward(s) / slices.Chunk(s, n)
maps.All(m) / maps.Keys(m) / maps.Values(m)
strings.Lines(s) / strings.FieldsSeq(s) / strings.SplitSeq(s, sep)  // Go 1.24+
bytes.Lines(b) / bytes.SplitSeq(b, sep)                             // Go 1.24+

// 消费
slices.Collect(seq)   // Seq → []E
slices.Sorted(seq)    // Seq → 排序后的 []E
maps.Collect(seq2)    // Seq2 → map
maps.Insert(m, seq2)  // 灌入已有 map

自定义容器提供 for range

func (s *Set[T]) All() iter.Seq[T] {
    return func(yield func(T) bool) {
        for k := range s.data {
            if !yield(k) { return }
        }
    }
}

for v := range set.All() { ... }

资源安全的迭代器

func ReadLines(path string) iter.Seq[string] {
    return func(yield func(string) bool) {
        f, err := os.Open(path)
        if err != nil { return }
        defer f.Close() // 消费者 break 也会执行到这里

        sc := bufio.NewScanner(f)
        for sc.Scan() {
            if !yield(sc.Text()) { return }
        }
    }
}

for line := range ReadLines("app.log") {
    if strings.Contains(line, "ERROR") { break } // 文件句柄不会泄漏
}

这是 push 模型最大的红利:清理逻辑写在迭代器自己身上,消费者无论正常结束还是提前 break,defer 都跑。

迭代器陷阱

陷阱 后果
消费完毕后再次 for range 同一个 Seq 多数迭代器是单次语义,第二次直接空结束
iter.Pull 不调 stop() goroutine 泄漏
yield 返回 false 后继续产出 违反契约,行为不可预期
迭代期间持有锁再调 yield 循环体若再抢同一把锁就死锁

8. 泛型类型别名 (Go 1.24)

Go 1.24 起类型别名也可以带类型参数(1.23 是 GOEXPERIMENT=aliastypeparams 实验特性):

// 旧包
package pkg1
type Set[K comparable] map[K]struct{}

// 新包:别名保持"类型同一性"
package pkg2
import "xxx/pkg1"

type Set[K comparable] = pkg1.Set[K] // pkg2.Set[int] 与 pkg1.Set[int] 是同一类型

用途:把泛型类型跨包迁移/再导出而不破坏类型等价(普通 type Set[K comparable] pkg1.Set[K] 会产生新类型,互转需要显式转换)。别名还能部分实例化:

type StringSet = Set[string]
type OrderedSet[K cmp.Ordered] = Set[K] // 别名可以收窄约束

9. 递归泛型约束 (Go 1.26)

Go 1.26 允许约束接口引用自身(F-bounded 多态):

// Go 1.26 之前:循环自引用,编译错误
// Go 1.26:合法
type Adder[A Adder[A]] interface {
    Add(A) A
}

func SumAll[A Adder[A]](values []A) A {
    if len(values) == 0 {
        var zero A
        return zero
    }
    result := values[0]
    for _, v := range values[1:] {
        result = result.Add(v)
    }
    return result
}

实际场景:CRDT 合并、树遍历、链式 Builder——凡是要表达"T 的操作结果仍是 T 自身"的:

type Mergeable[T Mergeable[T]] interface{ Merge(T) T }
type TreeNode[T TreeNode[T]] interface{ Children() []T }
type Builder[T Builder[T]] interface {
    WithName(string) T
    WithAge(int) T
}

之前禁止是因为编译器要先确定泛型的"形状",自引用让推导复杂化;1.26 的实现改进才放开的。用递归约束的库,go.mod 需要 go 1.26+。


10. 深层原理:Go 泛型是怎么编译的

很多资料把 Go 泛型说成"C++ 式完全单态化",这不准确。Go 1.18+ 的方案是字典式传递 + 按 GC shape 复用代码的折中:

源码:  Max[T cmp.Ordered](a, b T) T
编译:  只编译成一份带"字典参数"的通用实现
调用:  Max[int](3, 5) → 传入 int 的字典(类型信息、操作实现、相关类型布局)
       Max[string] → 传入 string 的字典
  • 同一个泛型函数不管实例化成什么类型,通常只有一份机器码,靠运行期传入的字典找到 ==、>、零值、指针位图等实现。
  • 编译器知道具体类型时可以直接展开(devirtualize),小函数常被内联成"看着像单态化"的代码。
  • 底层类型与 GC 指针布局相同的类型(如 int、MyInt、int64)可以共享同一份实例化代码。

由此得到的工程结论:

  1. 二进制膨胀远比 C++ 模板温和,但也不是零——字典本身、边界上的特化副本都有成本。
  2. 泛型调用不等于零开销:接口装箱/解装箱、字典间接访问可能阻止内联。真要极致热路径,把具体类型版本单独写出来(或用 //go:fix inline 辅助)反而更快。
  3. any 约束的泛型只是"编译期类型安全的 interface{}"——没有操作可用时,它不比接口快。

想深挖可读 Ian Lance Taylor 在 Reddit 上关于 dictionaries 的系列解答,以及 golang.org/design/generics-implementation-dictionaries。


11. 泛型常见陷阱

陷阱 1:类型参数不能直接类型断言

func Process[T any](v T) {
    // v.(string) → 编译错误:cannot use type assertion on type parameter value

    // 正确:先转 any 再断言
    if s, ok := any(v).(string); ok {
        fmt.Println("it's a string:", s)
    }
}

这不是刁难——T 的静态类型不是接口,v.(string) 语法上就不成立。真要运行时判型,any(v).(type) 加 switch 是出口。

陷阱 2:var zero T 是唯一拿零值的办法

func Pop[T any](s []T) (T, bool) { ... }

// 错误:return T()     —— 不能把类型参数当函数调用
// 错误:return nil     —— T 未知,无法赋 nil(返回 *T 时倒可以)
// 正确:
var zero T
return zero, false

陷阱 3:方法不能"重新发明"T

type Box[T any] struct{ v T }

// func (b *Box[T]) Do[T any](x T) T { ... }
// → T redeclared in this block(遮蔽接收者的 T)

// func (b *Box[T]) Do[U any]() T { ... }
// → 1.27+ 合法(引入了新类型参数 U,只要不撞名)

陷阱 4:泛型方法进不了接口(1.27 也不解决)

type Mapper interface {
    // Map[U any](f func(int) U) []U  → interface method must have no type parameters
}

且具体类型上的泛型方法不能满足任何接口方法集。想要"可注入/可 mock"的泛型能力,模式是:函数字段 + 包级泛型函数,或干脆接口只保留具体方法。

陷阱 5:comparable ≠ 一定能安全 ==

func Keys[K comparable, V any](m map[K]V) []K { ... } // 合法——map key 约束必须是 comparable
func Bad[K any, V any](m map[K]V) []K { ... }          // 编译错误:K must be comparable

真正危险的是:comparable 约束接受接口类型作为实例化目标,而接口里装着 slice/map 时,== 在运行时 panic。约束写 comparable 的公共 API,别假设调用方传入的每个具体类型运行期都判得安全。

陷阱 6:泛型结构体里存 any 字段的 JSON 反序列化

type Response[T any] struct {
    Code int `json:"code"`
    Data T   `json:"data"`
}

// Marshal/Unmarshal 对 Response[User] 完全正常——实例化后就是普通结构体。

真正的坑在别处:Response[any] 配合 json 解码,Data 是 map/[]any 动态类型,omitempty 对 struct 不生效(零值 struct 不省略)、泛型参数里嵌泛型时推断会退化成 any——这些与 json 语义相关,见 encoding-json.md。

陷阱 7:maps.Keys 返回值在 1.23 变了

(见 7.2 版本坑——升级时这是编译级 breaking,值得单列。)


12. 最佳实践

12.1 什么时候该用泛型

适合 不适合
类型安全的容器(Set、Stack、Ring buffer) 只有一两种类型、手写更直白
操作 slice/map 的工具函数 每个类型行为差异大(该用接口/多态)
通用算法(排序、搜索、去重、池化) 泛型让调用方阅读成本变高
统一 API 的响应壳 Resp[T] 约束最终写了 any、里面又做断言

最后一条是自检信号:如果你约束是 any、函数体里靠 any(v).(type) 干活——你其实在写 interface{},泛型没带来任何东西。

12.2 设计原则

  • 约束最小化:需要 == 用 comparable,需要排序才用 cmp.Ordered,能不碰"操作集"就碰都不碰。
  • 行为用函数参数注入,不要塞进约束:SortFunc(s, less) 优于要求 T 实现 Less。
  • 先 any 再收紧是错的——收紧约束破坏兼容。一开始就按"将来可能支持的一切"设计(比如接受 ~int 的约束,别锁死 int)。
  • 1.27 起优先用泛型方法表达链式操作,但库要兼容旧版本时保留包级函数并行存在。

12.3 命名与可读性

// 语义清晰
func Merge[K comparable, V any](a, b map[K]V) map[K]V { ... }
func Transform[T, U any](s []T, fn func(T) U) []U { ... }

// 灾难:F[A, B, C any](a A, b B) C ——没人知道 A B C 是什么

13. 与现有代码的互操作

13.1 从 interface{} 迁移

// 旧:反射遍历,运行时才报错
func ContainsOld(s any, v any) bool {
    slice := reflect.ValueOf(s)
    ...
}

// 新:编译期就排除非 slice
func ContainsNew[E comparable](s []E, v E) bool {
    for _, e := range s {
        if e == v { return true }
    }
    return false
}

迁移顺序:先把调用最稳定、类型最单一的(容器/工具函数)改泛型;接口方法签名(行为抽象)不要动——那是泛型帮不上忙的地方。

13.2 渐进共存

泛型与非泛型互相调用毫无障碍:

func findUser(names []string, target string) int {
    return slices.Index(names, target) // 旧函数里直接用泛型库
}

go fix / stdversion vet 能在升级时帮你定位"用了太新的泛型特性(如 1.26 递归约束、1.27 泛型方法)但 go.mod 版本不够"的文件——go test 默认已带 stdversion 检查(1.27+)。


14. 相关资源