Go 语言在 1.18 版本正式引入了泛型,这事儿在国内 Go 圈子里炸过一波。说实话,我一开始觉得"不就是模板嘛,C++早就有了",但真正上手写了几次之后发现------这东西的爽点,比你想象的多得多。
以前写 Go,想要一个函数同时处理 int、float64、string 的切片,你得写出三份几乎一模一样的代码,或者硬上 interface{} 然后做类型断言。说真的,写起来浑身难受。
有了泛型之后,一行代码搞定:
bash
func print[T any](arr []T) {
for _, v := range arr {
fmt.Print(v, " ")
}
fmt.Println("")
}
[T any] 里的 any 就相当于说"不管啥类型,你来吧"。同样的函数,int 切片能用,string 切片能用,float64 切片也能用。编译期就帮你把类型确定好了,性能和手写的版本没差。

comparable 是个好东西
写个查找算法的时候,你会发现 any 不够用了------== 操作符在泛型类型上是不一定能用的。这时候就需要 comparable 约束:
bash
func find[T comparable](arr []T, elem T) int {
for i, v := range arr {
if v == elem {
return i
}
}
return -1
}
讲道理,这种设计挺 Go 的------你明确告诉我这个类型支持什么操作,我才让你做。编译期就把问题堵死,不让你运行时踩坑。
用泛型手搓数据结构
泛型最大的价值之一,就是写数据结构。以前我们要写一个泛型的栈,要么上 interface{} 然后到处断言,要么用 go generate 生成代码。前者不安全,后者麻烦。
现在直接写:
bash
type stack[T any] []T
func (s *stack[T]) push(elem T) {
*s = append(*s, elem)
}
func (s *stack[T]) pop() {
if len(*s) > 0 {
*s = (*s)[:len(*s)-1]
}
}
func (s *stack[T]) top() *T {
if len(*s) > 0 {
return &(*s)[len(*s)-1]
}
return nil
}
注意这里 top() 返回的是指针,而不是值。为啥?因为泛型 T 的"零值"不确定------int 是 0,string 是空串,但指针的零值永远是 nil,用一个指针就能统一处理空栈的情况。这是我写的时候踩过的坑,希望对你有帮助。
双向链表的实现同理,用 node[T comparable] 来定义结点,泛型的 == 直接支持删除操作。之前写链表只能"一条路走到黑"用一个类型,现在想放 int 就放 int,想放 string 就放 string,贼灵活。
函数式编程三板斧

说完了数据结构,再聊聊函数式编程那套。这是 Go 社区之前用反射写了无数次、代码丑到没法看的东西。
Map------把一个切片映射成另一个切片,类型可以不一样:
bash
func gMap[T1 any, T2 any](arr []T1, f func(T1) T2) []T2 {
result := make([]T2, len(arr))
for i, elem := range arr {
result[i] = f(elem)
}
return result
}
比如把数字切片映射成中文数字字符串,或者把字符串全转大写,一行调用搞定。
Reduce------把一堆数据合成一个:
bash
func gReduce[T1 any, T2 any](arr []T1, init T2, f func(T2, T1) T2) T2 {
result := init
for _, elem := range arr {
result = f(result, elem)
}
return result
}
我个人觉得这个写法还不够优雅,每次都要传初始值,但总比写循环强吧?
Filter------按条件过滤:
bash
func gFilterIn[T any](arr []T, f func(T) bool) []T {
result := []T{}
for _, elem := range arr {
if f(elem) {
result = append(result, elem)
}
}
return result
}
你懂的,这三个函数一组合,数据处理就跟搭积木似的。
做个业务场景试试水
来看一个实际的例子。我们有个员工列表:
bash
type Employee struct {
Name string
Age int
Vacation int
Salary float32
}
想统计薪资超过 6000 的人数、年龄小于 30 岁的员工总薪资、全员休假天数......以前你得写好几个不同的循环。现在用泛型的一行解决:
bash
// 薪资超过 6000 的人数
highPay := gCountIf(employees, func(e Employee) bool {
return e.Salary >= 6000
})
// 年龄小于 30 的员工总薪资
youngPay := gSum(employees, func(e Employee) float32 {
if e.Age < 30 { return e.Salary }
return 0
})
干净、清晰、编译期类型安全。
这里还有一个细节------gSum 函数用了接口约束 Sumable,它限定了只能用于数值类型(int 系列和 float 系列),这样做的好处是不会出现把字符串拿来求和的荒谬操作。
小结一下
Go 的泛型不是最强大的(你说它比 C++ 模板弱我认),但它足够实用。没有奇技淫巧,没有模板元编程,就是解决"写重复代码"这个实际问题。
我个人觉得,每个 Go 开发者都应该尽快上手泛型。原因很简单------你的代码会变得更短、更安全、更容易维护。尤其在做工具库、写通用数据结构和算法的时候,泛型就是那个你用了就回不去的东西。
你现在项目里用上 Go 泛型了吗?评论区聊聊你的实战感受👇