go语言爱好者 @go
2026-05-27
Go 1.26 里,泛型已经不是“新特性”,而是写好 Go 的基本功
你要是经历过早些年的 Go 开发,大概率都懂那种烦躁。
明明是同一段逻辑、同一个循环、同一种处理方式,只因为这次拿到的是 []int,下次拿到的是 []string,你就得老老实实再抄一遍。代码没变,脑子也没变,变的只有类型。那种机械重复,真的很消磨人。
大家当然也不是没挣扎过。
最常见的“聪明办法”,就是上 interface{},也就是今天更常写的 any。表面上看,它好像让函数“通用”了;可实际上,你是把编译期的安全感,换成了运行时的侥幸。类型断言一漏,线上 panic 就来了。那种感觉很像走在一层打蜡过头的地板上:你知道大概率能走过去,但心里始终不踏实。
后来,事情终于变了。
Go 1.18 在 2022 年 3 月正式发布,把泛型带进了语言本体;而到了 Go 1.26,这已经不是“新鲜玩具”,而是写现代 Go 时很自然的一部分。Go 官方也在 1.26 中继续完善泛型能力,比如允许泛型类型在自己的类型参数列表中引用自身,这让一些更复杂的数据结构写起来顺手了不少。 (Go)
所以问题来了:Go 泛型到底在干什么?它又是怎么把那些重复劳动,从你手里接过去的?
核心概念:Go 泛型到底在帮你做什么?
把它想简单一点:
泛型就是让你在定义函数或数据结构时,先别把类型写死,而是留一个“占位符”。等真正调用时,再告诉编译器,这里到底是 int、string,还是某个自定义结构体。
它很像先造一个架子,至于上面放书、放光盘,还是放你那一堆攒了很多年的 Gopher 周边,等用的时候再说。
在 Go 里,泛型最关键的两块是:类型参数 和 类型约束。
1. 类型参数:先留个位置,类型稍后再定
类型参数写在函数名或类型名后面的方括号里。
func Identity[T any](value T) T {
return value
}
这里的 T 就是类型参数。它的意思很直接:先别急着问我是什么类型,等你真正调用这个函数时,我再告诉你。
这个例子虽然简单,但它很准确地说明了泛型的基本动作:把“类型”也变成函数签名的一部分。
顺带一提,Go 1.26 对泛型类型系统又往前推了一步。官方博客明确提到,泛型类型现在可以在自己的类型参数列表里引用自身,这让某些递归式、互相关联的数据结构实现起来更自然。这个改动不花哨,但很实用。 (Go)
2. 类型约束:泛型不是放飞自我,而是“带规则地通用”
如果只有类型参数,没有约束,泛型很快就会变成一锅粥。
Go 这点其实拿捏得很克制。它没有让泛型变成“什么都行”,而是通过约束来告诉编译器:允许哪些类型进来,进来之后又能做哪些操作。
最常见的几种:
any:什么都可以,但你在函数体里也几乎不能假设它支持什么操作。
comparable:只能是能用 == 和 != 比较的类型,这对 map 键之类的场景特别顺手。
自定义接口(类型集合):这才是泛型真正有意思的地方。你可以自己规定,哪些类型被允许。
type Numeric interface {
int | int64 | float64
}
func Sum[T Numeric](a, b T) T {
return a + b
}
这里的 Numeric 就是一个类型集合,意思是:T 只能是 int、int64 或 float64 之一。也正因为约束已经告诉编译器“这些都是数值类型”,所以 + 才能合法使用。
这种设计很像 Go 一贯的风格:不给你无限魔法,但把真正有用的能力交给你,而且让编译器继续替你守住边界。 (Go)
一个很值钱的小细节:~ 真不是装饰符
写约束时,很多人一开始会忽略 ~。但它其实很有用。
例如:
type IntegerLike interface {
~int
}
这表示不仅接受原生 int,也接受底层类型是 int 的自定义类型,比如:
type UserID int
这样你既保留了领域类型的表达力,又不会把泛型函数写得过死。这一点在项目规模上来后,会特别舒服。官方规范对这种“underlying type” 的约束写法有明确定义。 (Go)
用例一:slices 包,终于不用再重复造轮子了
泛型真正改变日常开发体验的地方,不只是“你能写泛型函数”了,而是 Go 标准库终于开始大规模给你现成工具了。
从 Go 1.21 开始,标准库新增了 slices 和 maps 包,专门提供基于泛型的常用操作。也就是说,那些过去你总得自己写一遍、团队里复制来复制去的小工具,如今很多已经官方内置了。 (Go)
这件事的意义其实很大。因为泛型一旦进了标准库,开发者的心态就会变:它不再是“高阶技巧”,而是“正常写法”。
过去你可能会有这种痛苦:
func FilterInts(s []int, f func(int) bool) []int { /* ... */ }
func FilterStrings(s []string, f func(string) bool) []string { /* ... */ }
逻辑一样,只有类型不同,写起来真像在抄作业。
如果标准库没有你想要的某个工具,比如函数式风格里常见的 Map,那你也可以很优雅地自己补一个,而且只写一次就够了。
func Map[T1, T2 any](s []T1, f func(T1) T2) []T2 {
result := make([]T2, len(s))
for i, v := range s {
result[i] = f(v)
}
return result
}
这类函数一旦写好,后面不管你处理的是整数、字符串、时间、结构体,还是一堆业务模型,调用方式都一样,而且全程受编译器保护。
这种体验,说白了就是:终于不用再为了“类型不同”而重复劳动了。
用例二:数据结构终于可以“按逻辑复用”,而不是“按类型复制”
另一个经典痛点,是数据结构。
队列就是队列,栈就是栈。无论里面放的是用户、任务、订单,还是别的什么东西,它们的行为逻辑并不会因为元素类型变了就跟着变。过去 Go 没泛型时,想把这些结构做得真正通用,几乎绕不开 interface{},然后你就得接受运行时断言、可读性下降、IDE 推断变弱这一整套副作用。
泛型出现之后,这件事终于顺了。
下面这个栈的例子,我顺手帮你把格式和细节整理了一下,代码会更接近真实项目里的写法:
package main
import "fmt"
type Stack[T any] struct {
elements []T
}
func NewStack[T any]() *Stack[T] {
return &Stack[T]{
elements: make([]T, 0),
}
}
func (s *Stack[T]) Push(element T) {
s.elements = append(s.elements, element)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.elements) == 0 {
var zero T
return zero, false
}
last := len(s.elements) - 1
element := s.elements[last]
s.elements = s.elements[:last]
return element, true
}
func main() {
type User struct {
Name string
}
userStack := NewStack[User]()
userStack.Push(User{Name: "Alice"})
userStack.Push(User{Name: "Bob"})
if popped, ok := userStack.Pop(); ok {
fmt.Printf("Popped User: %s\n", popped.Name)
}
}
这里最舒服的地方在于:
你弹出来的就是 User,而不是一个模糊的 any;IDE 能直接补全字段;编译器也能替你提前发现问题。那种“写完还得小心翼翼断言一次”的不安感,基本就没有了。
说到底,泛型并没有让这些数据结构“更高级”,它只是终于让它们回到了它们本来该有的样子:逻辑复用,但不牺牲类型安全。
再往前一步:什么时候该用泛型,什么时候别硬上?
这里我得说一句很 Go 的话:泛型确实好用,但别上头。
它是工具,不是信仰。
Go 的魅力一直在于克制。很多时候,代码能写得更抽象,不等于代码就更好。泛型要用在“能明显减少重复、又不牺牲可读性”的地方;如果只是为了显得自己会写抽象,那就容易把一段本来很直接的代码,拧成一根别人读不懂的麻花。
几个比较稳的习惯,我建议保留:
先看标准库。
真的,先看 slices、maps,很多事官方已经给你写好了。别还没翻文档,就先起手一个自定义泛型工具箱。 (Go Packages)
能让编译器推断,就别手写类型参数。
比如这两种写法:
result := Map[int, int](mySlice, Double)
和
result := Map(mySlice, Double)
多数时候后者更自然。Go 的类型推断能力本来就是拿来省事的,不用白不用。官方也专门写过类型推断的说明。 (Go)
约束别写太死。
该用 ~ 的地方用上,让自定义领域类型也能吃到泛型函数的好处,而不是被排除在外。 (Go)
如果函数永远只处理字符串,那就写字符串函数。
别为了“理论上可泛化”就强行上泛型。Go 仍然是那个重可读性、重直接表达的语言,这点没变。
最后收个尾:泛型不是炫技,它只是让 Go 终于补上了那块空白
回头看,Go 1.18 把泛型带进来,确实是个分水岭;到了 Go 1.21,slices 和 maps 进入标准库,泛型开始真正影响日常写法;而 Go 1.26 又继续把这套能力往更成熟的方向推。现在的现实已经很清楚:泛型不是可选的“新语法”,而是现代 Go 工具箱里该有的一把趁手扳手。 (Go)
它最实在的价值,不是什么“学会了一个高级概念”,而是:
你终于不用再为类型差异,去复制那些一模一样的逻辑;
你终于不用再靠 interface{} 和类型断言赌线上运气;
你终于可以把复用、可读性、类型安全这三件事,同时拿在手里。
真要开始上手,也别搞太大。
挑一个你项目里反复出现的小工具函数,比如 Contains、IndexOf、Map,先改一个。你很快就会发现,泛型最迷人的地方从来不是“厉害”,而是它把那些原本很烦、很碎、很机械的重复劳动,安安静静地收掉了。
这才是它最值钱的地方。