从 Go 1.18 开始,泛型(Generics)终于正式进入语言本身。这个特性一出现,就迅速成为 Go 社区最受关注的话题之一。
在泛型出现之前,Go 程序员要解决“同一套逻辑适配多种类型”的问题,通常只有两条路:
- 要么重复写很多份类型几乎相同的代码
- 要么使用
interface{}(在 Go 1.18 之后通常写作any)再配合类型断言
前者的问题是代码重复、维护成本高;后者的问题是类型信息丢失,编译器帮不上太多忙,很多错误只能拖到运行时暴露。
泛型的价值,正是在这两者之间提供了一种更平衡的方案:既保留静态类型的安全性,又减少重复代码。
不过,泛型并不是“用了就一定更高级”。它是一种能力,也是一种成本。真正重要的不是“会不会写泛型”,而是:
- 什么场景适合用泛型
- 什么场景不该为了抽象而抽象
- 如何把泛型写得清晰,而不是把简单问题复杂化
这一章我们会围绕下面四个主题展开:
- 类型参数与类型约束
- 泛型函数与泛型类型
constraints包- 泛型最佳实践与适用场景
文章会重点对比泛型出现前后的写法差异,帮助你真正理解:Go 的泛型到底解决了什么问题,又没有解决什么问题。
一、为什么 Go 需要泛型
先看一个最常见的问题:求两个整数中的较大值。
如果没有泛型,我们会这样写:
package main
import "fmt"
func MaxInt(a, b int) int {
if a > b {
return a
}
return b
}
func MaxFloat64(a, b float64) float64 {
if a > b {
return a
}
return b
}
func main() {
fmt.Println(MaxInt(3, 5))
fmt.Println(MaxFloat64(3.14, 2.71))
}
这段代码没有问题,但也很明显:MaxInt 和 MaxFloat64 的逻辑几乎完全一样,只有类型不同。
如果后面还要支持 int64、uint、string,那就得继续写:
MaxInt64MaxUintMaxString
这就是泛型出现前最典型的代码重复。
另一种写法是使用空接口:
package main
import "fmt"
func MaxAny(a, b any) any {
av, ok1 := a.(int)
bv, ok2 := b.(int)
if !ok1 || !ok2 {
panic("只支持 int")
}
if av > bv {
return av
}
return bv
}
func main() {
fmt.Println(MaxAny(3, 5))
}
这种写法的问题也很明显:
- 参数虽然看起来“通用”,但其实靠运行时断言兜底
- 一旦传错类型,错误会在运行时暴露
- 返回值也是
any,调用方还可能继续做类型断言 - 可读性并不理想
泛型的目标,不是让 Go 变成“模板元编程大舞台”,而是解决这类重复但有规律的类型化代码。
二、类型参数与类型约束
1. 什么是类型参数
泛型最核心的变化,是函数或类型可以接收“类型”作为参数。
先看一个最基础的泛型函数:
package main
import "fmt"
func PrintValue[T any](v T) {
fmt.Printf("值:%v,类型:%T\n", v, v)
}
func main() {
PrintValue[int](100)
PrintValue[string]("hello")
PrintValue[bool](true)
}
这里的 T 就是类型参数。
PrintValue[T any] 可以拆开理解:
T:类型参数名any:类型约束,表示T可以是任意类型
所以这段代码的意思是:
PrintValue是一个带类型参数T的函数,而T可以是任意类型。
在调用时,可以显式写出类型参数:
PrintValue[int](100)
很多时候,Go 编译器还能自动推断类型参数,因此也可以直接写成:
PrintValue(100)
PrintValue("hello")
2. 为什么只写类型参数还不够
如果只是“接受任意类型”,那泛型的价值其实有限。真正关键的是:你希望这个类型具备什么能力。
例如,我们想写一个返回较大值的通用函数:
func Max[T any](a, b T) T {
if a > b {
return a
}
return b
}
这段代码实际上无法通过编译,因为 T any 只表示“任意类型”,并不保证这个类型支持 > 运算。
也就是说,编译器会追问一句:
你说
T可以是任意类型,那如果调用方传入结构体、切片或者 map,>该怎么比较?
这就引出了第二个核心概念:类型约束(constraint)。
3. 什么是类型约束
类型约束本质上是在告诉编译器:
这个类型参数虽然不是某一个具体类型,但它必须落在某个允许的类型集合里。
下面是一个可运行示例,我们自己定义一个“可比较大小的数值类型”约束:
package main
import "fmt"
type Number interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64
}
func Max[T Number](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
fmt.Println(Max(10, 20))
fmt.Println(Max(3.14, 2.71))
}
这个例子里:
Number是一个约束接口T Number表示:类型参数T必须满足Number这个约束- 因为
Number中列出的类型都支持>运算,所以Max可以合法比较
4. ~ 是什么意思
在类型约束中,~ 是一个很容易让人困惑、但非常重要的符号。
比如:
int表示类型必须就是int~int表示底层类型(underlying type)是int的类型也可以
下面看一个完整示例:
package main
import "fmt"
type MyInt int
type IntLike interface {
~int
}
func Double[T IntLike](v T) T {
return v * 2
}
func main() {
var a int = 10
var b MyInt = 20
fmt.Println(Double(a))
fmt.Println(Double(b))
}
如果这里把 ~int 改成 int,那么 MyInt 就不满足约束,因为它不是“就是 int”,而是“底层类型是 int 的自定义类型”。
所以可以把 ~ 理解成:
不只接受这个具体类型,也接受底层类型与它一致的自定义类型。
这在泛型设计里非常实用,因为它能让你的泛型函数对用户自定义类型更友好。
5. 约束接口和普通接口的区别
很多 Go 开发者第一次看到泛型约束时,会觉得它长得很像接口,于是容易误解成“这不就是接口吗?”
它们确实都写成 interface,但用途不同:
| 对比项 | 普通接口 | 泛型约束接口 |
|---|---|---|
| 主要用途 | 描述行为(方法集) | 限定类型参数可用的类型集合 |
| 关注点 | 某个类型会做什么 | 某个类型属于哪一类 |
| 常见内容 | 方法定义 | 类型并集、方法集,或两者结合 |
| 使用位置 | 变量、参数、返回值 | 类型参数约束 |
例如,普通接口常这样写:
package main
import "fmt"
type Speaker interface {
Speak()
}
type Dog struct {
Name string
}
func (d Dog) Speak() {
fmt.Println(d.Name, "汪汪叫")
}
func SaySomething(s Speaker) {
s.Speak()
}
func main() {
SaySomething(Dog{Name: "旺财"})
}
而泛型约束接口更关注“允许哪些类型”,例如前面的 Number。
简单说:
- 普通接口解决的是多态问题
- 泛型约束解决的是复用同构逻辑的问题
这两者经常一起出现,但并不是互相替代的关系。
三、泛型函数与泛型类型
1. 泛型函数:把“重复逻辑”提取出来
泛型函数最常见的适用场景,是算法逻辑不变,但处理的数据类型可以变化。
下面先看一个“泛型出现前”的切片查找例子:
package main
import "fmt"
func ContainsInt(items []int, target int) bool {
for _, item := range items {
if item == target {
return true
}
}
return false
}
func ContainsString(items []string, target string) bool {
for _, item := range items {
if item == target {
return true
}
}
return false
}
func main() {
fmt.Println(ContainsInt([]int{1, 2, 3}, 2))
fmt.Println(ContainsString([]string{"go", "java", "rust"}, "go"))
}
这段代码的问题很典型:逻辑一模一样,只是类型不同。
用泛型改写后会自然很多:
package main
import "fmt"
func Contains[T comparable](items []T, target T) bool {
for _, item := range items {
if item == target {
return true
}
}
return false
}
func main() {
fmt.Println(Contains([]int{1, 2, 3}, 2))
fmt.Println(Contains([]string{"go", "java", "rust"}, "go"))
}
这里的 T comparable 很关键。
因为函数内部用了 ==,所以 T 必须是可比较类型。像切片、map、函数这类不能直接用 == 比较的类型,就不满足 comparable。
2. comparable 是什么
comparable 是 Go 提供的一个预声明约束,用来表示:
该类型支持
==和!=比较。
这在泛型里非常常见,比如:
- 去重
- 查找
- map 的键类型约束
- 判等逻辑
下面是一个完整可运行的去重示例:
package main
import "fmt"
func Unique[T comparable](items []T) []T {
seen := make(map[T]struct{})
result := make([]T, 0, len(items))
for _, item := range items {
if _, ok := seen[item]; ok {
continue
}
seen[item] = struct{}{}
result = append(result, item)
}
return result
}
func main() {
fmt.Println(Unique([]int{1, 2, 2, 3, 1, 4}))
fmt.Println(Unique([]string{"go", "go", "rust", "go", "java"}))
}
这个例子很好地体现了泛型的价值:
- 逻辑只写一份
- 保留静态类型信息
- 调用时不需要类型断言
- 代码仍然清晰
3. 泛型类型:让数据结构可复用
泛型不仅能用于函数,也能用于类型定义。最典型的例子就是容器、集合、队列、栈等数据结构。
先看泛型出现前常见的写法:
package main
import "fmt"
type IntStack struct {
items []int
}
func (s *IntStack) Push(v int) {
s.items = append(s.items, v)
}
func (s *IntStack) Pop() (int, bool) {
if len(s.items) == 0 {
return 0, false
}
lastIndex := len(s.items) - 1
value := s.items[lastIndex]
s.items = s.items[:lastIndex]
return value, true
}
func main() {
var s IntStack
s.Push(10)
s.Push(20)
v, ok := s.Pop()
fmt.Println(v, ok)
}
如果还需要 StringStack、UserStack,又得再写很多份。
而有了泛型之后,可以直接把“元素类型”抽象成参数:
package main
import "fmt"
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
}
lastIndex := len(s.items) - 1
value := s.items[lastIndex]
s.items = s.items[:lastIndex]
return value, true
}
func main() {
var intStack Stack[int]
intStack.Push(10)
intStack.Push(20)
fmt.Println(intStack.Pop())
var stringStack Stack[string]
stringStack.Push("Go")
stringStack.Push("泛型")
fmt.Println(stringStack.Pop())
}
这里有两个细节值得注意:
Stack[T any]表示Stack是一个带类型参数的泛型类型Pop()里用了var zero T,这是获取类型参数零值的常见写法
4. 泛型类型配合方法使用
泛型类型并不只是“语法上能带 [T]”,它真正的意义在于:同一组行为可以稳定地作用于不同元素类型。
下面用一个简单的键值容器来演示:
package main
import "fmt"
type Pair[K comparable, V any] struct {
Key K
Value V
}
type Dictionary[K comparable, V any] struct {
data map[K]V
}
func NewDictionary[K comparable, V any]() *Dictionary[K, V] {
return &Dictionary[K, V]{
data: make(map[K]V),
}
}
func (d *Dictionary[K, V]) Set(key K, value V) {
d.data[key] = value
}
func (d *Dictionary[K, V]) Get(key K) (V, bool) {
value, ok := d.data[key]
return value, ok
}
func main() {
userScores := NewDictionary[string, int]()
userScores.Set("alice", 95)
userScores.Set("bob", 88)
score, ok := userScores.Get("alice")
fmt.Println(score, ok)
pair := Pair[string, int]{Key: "math", Value: 100}
fmt.Println(pair)
}
这里我们同时用到了两类泛型类型:
Pair[K, V]:通用键值对Dictionary[K, V]:通用字典容器
这类设计在工程里很常见,尤其适合:
- 集合容器
- 缓存封装
- 通用数据处理管道
- 事件载荷包装类型
5. 泛型不是为了消灭接口
很多人刚学泛型时,容易产生一个误区:
既然有泛型了,是不是很多接口都可以删掉了?
答案是否定的。
泛型更擅长处理的是:
- 相同算法逻辑
- 不同具体类型
- 并且这类类型之间通常不靠“方法行为”区分
而接口更擅长处理的是:
- 不同实现策略
- 基于行为的抽象
- 运行时多态
下面这个例子就很适合继续使用接口,而不是泛型:
package main
import "fmt"
type Payment interface {
Pay(amount float64) string
}
type Alipay struct{}
type WechatPay struct{}
func (Alipay) Pay(amount float64) string {
return fmt.Sprintf("支付宝支付 %.2f 元", amount)
}
func (WechatPay) Pay(amount float64) string {
return fmt.Sprintf("微信支付 %.2f 元", amount)
}
func Checkout(p Payment, amount float64) {
fmt.Println(p.Pay(amount))
}
func main() {
Checkout(Alipay{}, 99.9)
Checkout(WechatPay{}, 199.5)
}
这里关注的是“支付行为不同”,不是“同一逻辑适配不同基础类型”。所以接口依旧是更自然的抽象方式。
四、constraints 包
1. constraints 包是做什么的
在学习 Go 泛型时,很多资料都会提到 constraints 包。它的作用,是提供一些常见的类型约束定义,避免每次都手写一长串类型并集。
需要特别说明一点:
- 在很多教程里,
constraints通常指的是golang.org/x/exp/constraints - 它来自 Go 官方实验仓库
x/exp - 它并不是早期 Go 标准库中的正式稳定包
- 在较新的 Go 版本里,部分场景也可以考虑使用标准库里的
cmp.Ordered
也就是说,constraints 很常用,但要知道它的来源和定位。
2. 使用 constraints.Ordered
如果你想写一个支持大小比较的泛型函数,constraints.Ordered 会很方便。它表示支持 <、<=、>、>= 这些顺序比较的类型。
下面是一个完整可运行示例。
如果你在一个新项目中运行它,先执行:
go mod init generics-demo
go get golang.org/x/exp/constraints@latest
再运行下面这段代码:
package main
import (
"fmt"
"golang.org/x/exp/constraints"
)
func Min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
func main() {
fmt.Println(Min(10, 20))
fmt.Println(Min(3.14, 2.71))
fmt.Println(Min("go", "java"))
}
这里 constraints.Ordered 能支持的常见类型包括:
- 整数
- 无符号整数
- 浮点数
- 字符串
因为这些类型都支持顺序比较。
3. 使用 constraints.Integer 和 constraints.Float
constraints 包还提供了更细粒度的约束,比如:
constraints.Integerconstraints.Floatconstraints.Signedconstraints.Unsigned
这在你只想允许某一类数值类型时特别有用。
下面是一个只接受整数类型的完整示例:
package main
import (
"fmt"
"golang.org/x/exp/constraints"
)
func SumInts[T constraints.Integer](values []T) T {
var total T
for _, v := range values {
total += v
}
return total
}
func main() {
fmt.Println(SumInts([]int{1, 2, 3, 4}))
fmt.Println(SumInts([]int64{10, 20, 30}))
}
再看一个只接受浮点数的示例:
package main
import (
"fmt"
"golang.org/x/exp/constraints"
)
func Average[T constraints.Float](values []T) T {
if len(values) == 0 {
return 0
}
var total T
for _, v := range values {
total += v
}
return total / T(len(values))
}
func main() {
fmt.Println(Average([]float32{1.5, 2.5, 3.5}))
fmt.Println(Average([]float64{10.2, 20.4, 30.6}))
}
这种写法比自己手写一长串 ~float32 | ~float64 更直接,也更容易读懂。
4. constraints 包和自己定义约束,怎么选
实际开发中,这两种方式都很常见。
| 方式 | 优点 | 适用场景 |
|---|---|---|
使用 constraints 包 |
语义清晰、代码更短、复用现成约束 | 常见数值/有序类型约束 |
| 自己定义约束 | 更灵活,可表达业务专属类型集合 | 需要定制类型范围或额外方法要求 |
比如,如果你只是要表达“能比较大小”,那 constraints.Ordered 就很省事。
但如果你想表达的是:
- 只允许你的某几类业务 ID 类型
- 既要底层是整数,又要实现某个方法
- 只允许特定范围的自定义类型
那么自己定义约束通常更合适。
下面是一个“自定义业务约束”的完整例子:
package main
import "fmt"
type UserID int64
type OrderID int64
type BizID interface {
~int64
}
func FormatID[T BizID](prefix string, id T) string {
return fmt.Sprintf("%s-%d", prefix, id)
}
func main() {
var userID UserID = 1001
var orderID OrderID = 90001
fmt.Println(FormatID("user", userID))
fmt.Println(FormatID("order", orderID))
}
这个例子体现的是:泛型约束不只是服务于“算法”,也能服务于类型安全的业务建模。
5. 新版本里也可以关注 cmp.Ordered
从学习路径上说,你会看到很多文章使用 constraints.Ordered;这没有问题,因为它确实是 Go 泛型早期教程里最常见的写法。
不过在现代 Go 代码里,也可以关注标准库 cmp 包中的 cmp.Ordered。例如:
package main
import (
"cmp"
"fmt"
)
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
fmt.Println(Max(7, 9))
fmt.Println(Max("apple", "banana"))
}
这段代码不依赖 x/exp,对于现代 Go 项目来说也很实用。
这里的重点不是“必须选哪一个”,而是要明白:
constraints包是学习和实践泛型约束的重要工具- 在不同 Go 版本和项目环境下,具体选择可以略有差异
五、泛型最佳实践与适用场景
泛型真正难的,不是语法,而是“克制”。
很多代码在“能写成泛型”和“应该写成泛型”之间,差得非常远。
1. 什么时候适合用泛型
通常来说,下面这些场景很适合使用泛型:
| 场景 | 说明 |
|---|---|
| 通用数据结构 | 例如栈、队列、集合、缓存容器 |
| 通用算法 | 例如查找、过滤、映射、去重、聚合 |
| 类型安全的工具函数 | 例如 Min、Max、Contains、MapKeys |
| 重复代码只因类型不同 | 逻辑完全一致,仅输入输出类型变化 |
下面是一个很典型的“通用映射”函数示例:
package main
import "fmt"
func MapSlice[T any, R any](items []T, mapper func(T) R) []R {
result := make([]R, 0, len(items))
for _, item := range items {
result = append(result, mapper(item))
}
return result
}
func main() {
nums := []int{1, 2, 3, 4}
squares := MapSlice(nums, func(v int) int {
return v * v
})
words := []string{"go", "泛型", "map"}
lengths := MapSlice(words, func(s string) int {
return len(s)
})
fmt.Println(squares)
fmt.Println(lengths)
}
这个例子就很适合用泛型,因为:
- 遍历逻辑完全一致
- 输入类型和输出类型都可能变化
- 不使用泛型就很容易出现大量重复函数
2. 什么时候不适合用泛型
有些场景看起来也能用泛型,但其实不值得。
最常见的几类情况:
情况一:代码根本不重复
如果某段逻辑只会写一次,或者业务语义非常具体,那就没必要为了“看起来高级”强行做泛型抽象。
例如:
- 用户下单校验
- 支付回调签名验证
- 某个具体业务报表生成
这类代码的核心复杂度来自业务规则,不来自“类型重复”。泛型帮不上太多忙。
情况二:本质上应该用接口做行为抽象
如果你的问题是“不同对象有不同做法”,那通常应该先考虑接口。
例如:
- 不同存储实现:MySQL、Redis、S3
- 不同通知方式:短信、邮件、Push
- 不同支付渠道:支付宝、微信、银行卡
这类问题的核心是行为差异,不是“同一算法适配多种类型”。
情况三:泛型让代码更难懂
如果一个泛型定义需要:
- 很长的约束链
- 多层嵌套类型参数
- 很不直观的推导关系
那你就应该停下来问一句:
这段代码到底是在减少复杂度,还是把复杂度藏起来?
Go 的风格一直是“简单优先”。如果泛型让读者更难理解,那通常就不是一个好抽象。
3. 最佳实践一:优先解决真实重复,而不是预判重复
这是使用泛型时最重要的一条建议。
很多人刚接触泛型时,容易一开始就设计出非常通用、非常抽象的组件,结果后来发现:
- 实际上只有一个地方会用
- 约束设计过重
- 调用方式反而更复杂
更推荐的做法是:
- 先写出清晰的具体代码
- 当你看到第二份、第三份几乎相同的实现时
- 再把重复逻辑抽象成泛型
也就是说,泛型应该服务于已经出现的重复,而不是猜测未来可能发生的重复。
4. 最佳实践二:约束尽量小而清晰
一个很好的泛型约束,通常具备两个特点:
- 足够表达当前需求
- 不额外引入无关能力
例如,如果你只需要 ==,那就用 comparable;如果只需要顺序比较,就用 constraints.Ordered 或 cmp.Ordered;不要一上来就定义一个覆盖大量类型的大杂烩约束。
下面是一个“约束过重”和“约束恰当”的对比:
| 写法 | 问题/优点 |
|---|---|
func Contains[T any](...) |
过于宽泛,函数内部无法安全使用 == |
func Contains[T comparable](...) |
约束恰当,正好满足判等需求 |
约束越精确,调用方越容易理解你的意图。
5. 最佳实践三:类型参数命名要有可读性
简单场景里,Go 社区常用单字母命名:
T:TypeK:KeyV:ValueR:Result
这在短小泛型函数里是没问题的。
但如果一个定义比较复杂,或者有多个类型参数,适当使用更有语义的名字会更清晰。比如:
InputOutputElemNumber
下面是一个可运行示例:
package main
import "fmt"
func ConvertSlice[Input any, Output any](items []Input, converter func(Input) Output) []Output {
result := make([]Output, 0, len(items))
for _, item := range items {
result = append(result, converter(item))
}
return result
}
func main() {
values := []int{1, 2, 3}
texts := ConvertSlice(values, func(v int) string {
return fmt.Sprintf("值=%d", v)
})
fmt.Println(texts)
}
这个例子里,Input 和 Output 就比 T、R 更直观。
6. 最佳实践四:不要把所有工具函数都泛型化
有些函数虽然理论上可以写成泛型,但业务上并不值得。
例如一个只在订单模块内部使用、只处理 Order 的排序函数:
func SortOrdersByAmount(orders []Order) {
// ...
}
这类函数很具体、很明确,就保持具体即可。
泛型的目标是提升复用性,不是把每个函数都变成模板。
7. 最佳实践五:把“编译期安全”作为核心收益来理解
很多人学习泛型时,容易只看到“少写几份代码”。这当然是收益,但不是全部。
更大的价值其实是:
- 编译器知道参数和返回值的真实类型
- 调用方不用做类型断言
- 错误更容易在编译期暴露
- IDE 补全、重构、静态分析会更准确
下面用一个简单例子做对比。
使用 any 的写法:
package main
import "fmt"
func FirstAny(items []any) any {
if len(items) == 0 {
return nil
}
return items[0]
}
func main() {
value := FirstAny([]any{100, 200, 300})
number, ok := value.(int)
fmt.Println(number, ok)
}
使用泛型的写法:
package main
import "fmt"
func First[T any](items []T) (T, bool) {
var zero T
if len(items) == 0 {
return zero, false
}
return items[0], true
}
func main() {
value, ok := First([]int{100, 200, 300})
fmt.Println(value, ok)
}
第二种写法的优势很明确:
- 返回值仍然是
int - 不需要断言
- 错误更少,使用体验更自然
这也是为什么很多原本依赖 interface{} 的通用工具代码,在 Go 1.18 之后都很适合重构成泛型。
六、一个总结:泛型解决了什么,没有解决什么
学到这里,你可以把泛型的价值归纳为下面这张表:
| 问题 | 泛型是否擅长解决 | 说明 |
|---|---|---|
| 多种基础类型上的重复算法 | 是 | 例如 Max、Contains、Unique |
| 通用数据结构复用 | 是 | 例如栈、队列、集合、字典 |
避免 any 带来的类型断言 |
是 | 提升编译期类型安全 |
| 不同实现之间的行为切换 | 否,通常更适合接口 | 例如支付、存储、通知策略 |
| 复杂业务规则抽象 | 不一定 | 业务复杂度通常不是类型参数能解决的 |
| 让代码“更高级” | 不是目标 | 如果更难懂,就不值得 |
所以,一个比较稳妥的理解方式是:
泛型是 Go 用来表达“相同逻辑、不同类型”的工具,而接口是 Go 用来表达“相同行为、不同实现”的工具。
这两者各有边界,各有优势。真正成熟的 Go 代码,不是“到处都用泛型”,而是知道什么时候该用、什么时候该停。
如果你已经有 Go 基础,那么在学习泛型时,最值得建立的不是语法记忆,而是这三个判断标准:
- 这里是否真的存在重复逻辑?
- 这里的问题本质是类型复用,还是行为抽象?
- 引入泛型之后,代码是更清晰了,还是更绕了?
只要把这三个问题想清楚,泛型就会成为你的工具,而不是负担。
📝 版权声明:本文为原创技术博客,转载请注明出处。
如文章中存在错误或不准确之处,欢迎在评论区指正,感谢您的阅读与支持!