为什么 2022 年大家都在讨论泛型
如果你在 2022 年学习 Go,很难绕开一个话题:Go 1.18 带来了泛型。对很多从 Java、C#、TypeScript 转过来的人来说,泛型并不陌生;但对长期写 Go 的人来说,这是语言风格里一次很重要的变化。过去 Go 社区习惯用具体类型、小接口、代码生成和适度重复来解决复用问题。泛型出现后,一些集合类和算法类代码终于可以写得更自然。
不过,泛型并不是“终于可以把 Go 写成另一门语言”的许可。Go 的核心审美仍然是简单、清楚、直接。泛型最适合处理那些逻辑完全一样、只是元素类型不同的代码,比如查找、过滤、映射、集合、栈、队列。普通业务函数如果本来就只服务一个类型,就没必要为了新特性强行抽象。
这篇文章只讲三个小函数:Contains、Map 和一个简单 Set。它们足够展示泛型解决的问题,也不会把你带进复杂类型系统。
从重复的 Contains 开始
没有泛型时,你可能写过这种函数:
func ContainsString(items []string, target string) bool {
for _, item := range items {
if item == target {
return true
}
}
return false
}
func ContainsInt(items []int, target int) bool {
for _, item := range items {
if item == target {
return true
}
}
return false
}
两段函数完全一样,只是类型不同。泛型可以这样写:
func Contains[T comparable](items []T, target T) bool {
for _, item := range items {
if item == target {
return true
}
}
return false
}
调用:
fmt.Println(Contains([]string{"Go", "PHP", "Python"}, "Go"))
fmt.Println(Contains([]int{1, 2, 3}, 2))
T 是类型参数,comparable 是约束。因为函数里用了 ==,所以元素类型必须支持比较。Go 不允许你对任意类型使用 ==,比如切片和 map 就不能直接比较。约束让编译器知道这个泛型函数里允许哪些操作。
any 表示没有额外要求
再写一个取第一个元素的函数:
func First[T any](items []T) (T, bool) {
var zero T
if len(items) == 0 {
return zero, false
}
return items[0], true
}
any 表示没有额外约束。这个函数不比较、不加减、不调用方法,只是返回切片里的元素,所以任何类型都可以。
调用:
name, ok := First([]string{"小林", "阿周"})
if ok {
fmt.Println(name)
}
这里有一个细节:空切片时要返回 zero。泛型函数里你不知道 T 到底是什么类型,所以需要用 var zero T 得到它的零值。对 string 是空字符串,对 int 是 0,对指针是 nil。
初学者容易误以为 any 就是什么操作都能做。不是这样。下面代码不能编译:
func Add[T any](a, b T) T {
return a + b
}
因为 any 没有告诉编译器 T 支持 +。
Map:把一种切片变成另一种切片
func Map[T any, R any](items []T, convert func(T) R) []R {
result := make([]R, 0, len(items))
for _, item := range items {
result = append(result, convert(item))
}
return result
}
使用:
type User struct {
ID int64
Name string
}
users := []User{
{ID: 1, Name: "小林"},
{ID: 2, Name: "阿周"},
}
names := Map(users, func(user User) string {
return user.Name
})
fmt.Println(names)
这里有两个类型参数:T 是输入元素类型,R 是输出元素类型。Map 的逻辑很稳定:遍历输入,调用转换函数,把结果放进新切片。
这种工具函数适合简单转换。如果转换逻辑很复杂,或者链式调用很多层,普通 for 循环可能更清楚。Go 的泛型不是为了让所有代码都函数式化,而是减少那些明确、机械、重复的代码。
一个简单 Set
Go 没有内置 Set,但可以用 map 实现:
type Set[T comparable] struct {
items map[T]struct{}
}
func NewSet[T comparable]() *Set[T] {
return &Set[T]{
items: make(map[T]struct{}),
}
}
func (s *Set[T]) Add(item T) {
s.items[item] = struct{}{}
}
func (s *Set[T]) Has(item T) bool {
_, ok := s.items[item]
return ok
}
func (s *Set[T]) Len() int {
return len(s.items)
}
使用:
tags := NewSet[string]()
tags.Add("Go")
tags.Add("Backend")
fmt.Println(tags.Has("Go"))
fmt.Println(tags.Len())
没有泛型时,你可能写 StringSet、IntSet,或者用 map[interface{}]struct{}。前者重复,后者损失类型安全。泛型版本既复用代码,又保留类型检查。
什么时候不要急着用泛型
如果你只是写一个用户注册函数:
func RegisterUser(input RegisterInput) error {
}
不要为了“抽象”写成:
func Register[T Validatable](input T) error {
}
除非你真的有多个类型共享同一套稳定流程,并且泛型让调用方更清楚,否则具体函数更好。业务代码最重要的是语义,不是复用形式。
一个实用判断是:泛型是否让调用处更简单?是否保留了类型安全?是否减少了真实重复?如果只是让函数签名更难读,那就先不用。
小结
Go 1.18 的泛型主要通过类型参数和约束解决复用问题。any 表示没有额外约束,comparable 表示可以比较,类型参数能让 Contains、Map、Set 这类工具函数写得更自然。
初学阶段不要从复杂约束开始,先从小函数练手。你会更容易理解泛型的真正价值:不是炫技,而是在合适的地方减少重复,同时保持 Go 代码一贯的清楚和类型安全。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。