Go 泛型

Go 类型参数与约束让函数/类型在多具体类型上复用逻辑并保留静态类型信息;设计克制,优先减少样板而非元编程。

#type / concept #status / growing #tech / dev #resource / go

[!info] 关联笔记

Go 泛型

这个概念为什么会出现

在泛型进入语言之前,复用“算法相同、元素类型不同”的代码常见三条路:

  1. 复制粘贴 MinInt / MinFloat64
  2. interface{} / any + 类型断言 → 丢静态检查、运行时才炸
  3. 代码生成 → 工具链与可读性成本

Go 1.18 引入类型参数(type parameters):在编译期用约束(constraint)描述“类型必须具备的能力”,实例化后仍是具体类型,避免把所有东西都收成空接口。

设计目标刻意克制:减少样板、服务数据结构与算法库,而不是把 Go 变成高阶类型体操语言。

[!abstract] 一句话理解 泛型用类型参数在编译期参数化函数与类型:约束规定可用操作,实例化后保留静态类型安全,适合容器与算法,不适合替代一切接口多态。

最小可运行示例

先把示例放进业务场景,再看代码:

场景 A:限流/库存里取“较小预算”

下单时同时有“用户剩余额度”和“SKU 剩余库存”,要取两者较小值。
额度可能是 int,价格/重量可能是 float64——
不想为每种数字写一遍 minInt/minFloat,又要保留静态类型。

场景 B:把订单 ID 列表格式化成展示标签

[]int{1,2,3}[]string{"#1","#2","#3"}
映射逻辑与元素类型无关,适合 MapSlice[T,U]

package main

import (
	"cmp"
	"fmt"
)

// minBudget 模拟“取两个可比较量的较小者”。
// 业务:min(用户额度, 库存) / min(两路超时) 等。
// 教学点:T 受 cmp.Ordered 约束,调用处可推断 T。
func minBudget[T cmp.Ordered](a, b T) T {
	if cmp.Less(a, b) {
		return a
	}
	return b
}

// mapSlice 模拟“把一列领域值映射成另一列展示值”。
// 教学点:两个类型参数 T/U;f 描述元素变换。
func mapSlice[T any, U any](in []T, f func(T) U) []U {
	out := make([]U, len(in))
	for i, v := range in {
		out[i] = f(v)
	}
	return out
}

func main() {
	// 场景 A:额度与库存都是 int
	fmt.Println("allow:", minBudget(3, 5)) // 3
	// 同一函数用于 float64 价格
	fmt.Println("cheaper:", minBudget(2.5, 1.2)) // 1.2

	// 场景 B:订单 ID → 展示标签
	labels := mapSlice([]int{1, 2, 3}, func(id int) string {
		return fmt.Sprintf("#%d", id)
	})
	fmt.Println(labels) // [#1 #2 #3]
}

cmp 时也可用自定义约束(演示 ~ 与 union):

type Ordered interface {
	~int | ~int64 | ~float64 | ~string
}

建议运行:

go run .

期望输出:

allow: 3
cheaper: 1.2
[#1 #2 #3]

结合场景再看三个关注点

  1. [T cmp.Ordered] 是编译期约束,不是运行时 interface 盒子。
  2. 调用处常可推断 T,不必写 minBudget[int](3,5)
  3. any 表示无约束;真正需要比较/运算时用更窄约束(如 Ordered)。

核心概念与准确模型

类型参数 vs 接口

泛型类型参数接口
检查时机编译期实例化动态(方法集)+ 编译期赋值检查
性能心智通常单态/特化相关实现策略接口调用可能间接
典型用途容器、算法、零样板工具行为多态、依赖倒置
表达“这是一个 T 的 slice”“这是某个实现了 Reader 的值”

二者互补:接口描述行为;泛型描述结构与算法对类型参数的依赖

约束(constraints)

// 方法集约束
type Stringer interface {
	String() string
}

// 类型元素(union)与底层类型近似 ~T
type Signed interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64
}

func PrintStringer[T Stringer](v T) { fmt.Println(v.String()) }
  • ~int 表示底层类型为 int 的所有类型(含 type Age int)。
  • 约束本身就是接口;可嵌套组合。

泛型类型

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

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

func (s *Stack[T]) Pop() (T, bool) {
	var zero T
	if len(s.items) == 0 {
		return zero, false
	}
	i := len(s.items) - 1
	v := s.items[i]
	s.items = s.items[:i]
	return v, true
}

方法不能额外声明新的类型参数(接收者类型已参数化);包级函数可以。

推断与显式实例化

Min[int](1, 2) // 显式
Min(1, 2)      // 推断

复杂嵌套有时需显式传类型实参。

标准库中的泛型

用途
slices泛型切片算法
mapsmap 键值工具
cmp有序比较
sync.Map未必泛型;按包文档

优先用标准库,避免自造平行宇宙。

实现向提示(非规范)

编译器/运行时如何实例化泛型(单态化、GC shape 等)是实现细节,随版本变化。编写代码时应依赖类型检查规则与可移植语义,不为某版本的特化策略写脏技巧。

边界情况与反直觉行为

1. 不能对类型参数随意用未在约束中的操作

func Bad[T any](a, b T) bool { return a < b } // 非法:any 不可比

必须把 Ordered 等能力写进约束。

2. 方法不能再引入类型参数

func (s *Stack[T]) Convert[U any]() {} // 非法

改用包级函数 Convert[T,U any](s *Stack[T]) ...

3. 类型推断失败场景

多返回、复杂嵌套、nil 无类型时需显式 [T]

4. 与反射/接口的交织

any 约束的值传入后若再装进接口,仍可能走反射式框架路径;泛型不是反射的替代品(go-reflection)。

5. API 膨胀

公开库每个函数都泛型化会提高用户认知负担。导出泛型 API 前问:是否至少两个真实类型需要?

常见误区

[!warning] 常见误区:用泛型重写所有接口 错误:Service[T] 满天飞替代行为多态。
正确:接口仍管“谁能做什么”;泛型管“对 T 的算法”。

[!warning] 常见误区:约束写成空 interface 却假定能比较/取值字段 错误:[T any] 里写 a.Field
正确:约束方法或改设计(传 accessor)。

[!warning] 常见误区:泛型一定更快 错误:无测断言“比 interface 快”。
正确:用 benchmark;可读性优先。

[!warning] 常见误区:与 Java/C++ 泛型一一对应 错误:套用通配符/偏特化思维。
正确:学 Go 自己的约束与推断规则。

工程实践

  1. 先复制两份具体实现,再抽泛型——确认同构。
  2. 约束最小化:只要求用到的操作。
  3. 优先标准库 slices/maps/cmp
  4. 错误与零值var zero T; return zero, err 是常见模式。
  5. 不要为测试而泛型:测试辅助可用,但生产 API 保持克制。
  6. 文档:说明类型参数含义与复杂度。
func Keys[M ~map[K]V, K comparable, V any](m M) []K {
	keys := make([]K, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	return keys
}

可验证实验

实验 1:Min 约束

去掉 Ordered,观察编译错误信息。

实验 2:自定义 ~int

type Age int~int 约束下可比较;无 ~int union 则拒收 Age

实验 3:Stack[T]

Stack[string]Stack[int] 分别 push/pop。

实验 4:与 interface 对比

any + 断言写 Min,对比丢失类型安全的失败模式。

本节总结

  • 本质:编译期类型参数 + 约束接口。
  • 用途:算法与数据结构去样板,保留类型信息。
  • 克制:不是元编程平台;接口仍是行为抽象主力。
  • 下一步:需要运行时摸类型结构时再学 反射

自测题

概念题

  1. ~intint 在约束里有何不同?
  2. 为何 Min[T any] 不能直接用 <
  3. 泛型与接口各解决什么问题?

代码推理题

func Ptr[T any](v T) *T { return &v }

这个模式常见用途?有何注意点?

工程思考题

团队想把所有 repository 都改成 Repo[T any]。如何评估是否过度?

参考答案

展开
  1. ~int 含底层类型为 int 的已定义类型;int 仅 int 本身。
  2. any 未提供可比较性;需 Ordered 等约束。
  3. 泛型:类型参数化的结构/算法;接口:运行时行为多态与解耦。
    代码题:取局部副本地址返回,用于语法上需要 *T 的 API;注意返回指针指向的是副本不是调用方变量。
    工程题:看是否真有多实体共享算法;若只是 CRUD 样板,接口+代码生成或小重复可能更清晰。

延伸阅读与资料来源

资料类型支撑
Type parameters proposal设计动机与规则
Tutorial: Getting started with generics教程上手
Spec — Type parameters规范形式定义
slices / cmp / maps标准库实践范本

笔记元信息

  • 建议文件名:go-generics.md
  • 所属阶段:阶段六
  • 学习顺序:接口熟练后
  • 建议下一篇:反射
  • 本篇状态:已深化(书章结构;约束与克制设计)
创建于 2026/6/20 更新于 2026/7/15