返回首页

Golang进阶:2.4 泛型编程(Go 1.18+)

从 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))
}

这段代码没有问题,但也很明显:MaxIntMaxFloat64 的逻辑几乎完全一样,只有类型不同。

如果后面还要支持 int64uintstring,那就得继续写:

  • MaxInt64
  • MaxUint
  • MaxString

这就是泛型出现前最典型的代码重复。

另一种写法是使用空接口:

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))
}

这种写法的问题也很明显:

  1. 参数虽然看起来“通用”,但其实靠运行时断言兜底
  2. 一旦传错类型,错误会在运行时暴露
  3. 返回值也是 any,调用方还可能继续做类型断言
  4. 可读性并不理想

泛型的目标,不是让 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)
}

如果还需要 StringStackUserStack,又得再写很多份。

而有了泛型之后,可以直接把“元素类型”抽象成参数:

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())
}

这里有两个细节值得注意:

  1. Stack[T any] 表示 Stack 是一个带类型参数的泛型类型
  2. 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.Integerconstraints.Float

constraints 包还提供了更细粒度的约束,比如:

  • constraints.Integer
  • constraints.Float
  • constraints.Signed
  • constraints.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. 什么时候适合用泛型

通常来说,下面这些场景很适合使用泛型:

场景 说明
通用数据结构 例如栈、队列、集合、缓存容器
通用算法 例如查找、过滤、映射、去重、聚合
类型安全的工具函数 例如 MinMaxContainsMapKeys
重复代码只因类型不同 逻辑完全一致,仅输入输出类型变化

下面是一个很典型的“通用映射”函数示例:

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. 最佳实践一:优先解决真实重复,而不是预判重复

这是使用泛型时最重要的一条建议。

很多人刚接触泛型时,容易一开始就设计出非常通用、非常抽象的组件,结果后来发现:

  • 实际上只有一个地方会用
  • 约束设计过重
  • 调用方式反而更复杂

更推荐的做法是:

  1. 先写出清晰的具体代码
  2. 当你看到第二份、第三份几乎相同的实现时
  3. 再把重复逻辑抽象成泛型

也就是说,泛型应该服务于已经出现的重复,而不是猜测未来可能发生的重复。

4. 最佳实践二:约束尽量小而清晰

一个很好的泛型约束,通常具备两个特点:

  • 足够表达当前需求
  • 不额外引入无关能力

例如,如果你只需要 ==,那就用 comparable;如果只需要顺序比较,就用 constraints.Orderedcmp.Ordered;不要一上来就定义一个覆盖大量类型的大杂烩约束。

下面是一个“约束过重”和“约束恰当”的对比:

写法 问题/优点
func Contains[T any](...) 过于宽泛,函数内部无法安全使用 ==
func Contains[T comparable](...) 约束恰当,正好满足判等需求

约束越精确,调用方越容易理解你的意图。

5. 最佳实践三:类型参数命名要有可读性

简单场景里,Go 社区常用单字母命名:

  • T:Type
  • K:Key
  • V:Value
  • R:Result

这在短小泛型函数里是没问题的。

但如果一个定义比较复杂,或者有多个类型参数,适当使用更有语义的名字会更清晰。比如:

  • Input
  • Output
  • Elem
  • Number

下面是一个可运行示例:

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)
}

这个例子里,InputOutput 就比 TR 更直观。

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 之后都很适合重构成泛型。


六、一个总结:泛型解决了什么,没有解决什么

学到这里,你可以把泛型的价值归纳为下面这张表:

问题 泛型是否擅长解决 说明
多种基础类型上的重复算法 例如 MaxContainsUnique
通用数据结构复用 例如栈、队列、集合、字典
避免 any 带来的类型断言 提升编译期类型安全
不同实现之间的行为切换 否,通常更适合接口 例如支付、存储、通知策略
复杂业务规则抽象 不一定 业务复杂度通常不是类型参数能解决的
让代码“更高级” 不是目标 如果更难懂,就不值得

所以,一个比较稳妥的理解方式是:

泛型是 Go 用来表达“相同逻辑、不同类型”的工具,而接口是 Go 用来表达“相同行为、不同实现”的工具。

这两者各有边界,各有优势。真正成熟的 Go 代码,不是“到处都用泛型”,而是知道什么时候该用、什么时候该停。

如果你已经有 Go 基础,那么在学习泛型时,最值得建立的不是语法记忆,而是这三个判断标准:

  1. 这里是否真的存在重复逻辑?
  2. 这里的问题本质是类型复用,还是行为抽象?
  3. 引入泛型之后,代码是更清晰了,还是更绕了?

只要把这三个问题想清楚,泛型就会成为你的工具,而不是负担。


📝 版权声明:本文为原创技术博客,转载请注明出处。

如文章中存在错误或不准确之处,欢迎在评论区指正,感谢您的阅读与支持!

上一篇

Golang进阶:2.3 并发模式与最佳实践

下一篇

Golang进阶:2.5 反射(Reflection)