Go 早期没有泛型。
这句话听起来很普通,但它曾经深刻影响过 Go 程序员写代码的方式。你想写一个 Index 函数,在切片里找某个值的位置。如果是 []int,你写一个 IndexInt;如果是 []string,你再写一个 IndexString;如果是 []float64,你又得写一个 IndexFloat64。
代码逻辑几乎一样,差别只是类型。
泛型解决的正是这个问题:
当一段逻辑与具体类型无关时,让类型也成为参数。
这篇文章从 Go Tour 里的两个例子出发,讲清楚 Go 泛型里最重要的两件事:
- 泛型函数:
func Index[T comparable](...) - 泛型类型:
type List[T any] struct { ... }
读完以后,你应该能看懂 Go 里的类型参数、约束、any、comparable,也能自己写一个简单的泛型链表。
1. 没有泛型之前:重复的函数
假设我们要在 []int 里找一个数字的位置:
func IndexInt(s []int, x int) int {
for i, v := range s {
if v == x {
return i
}
}
return -1
}
这个函数很简单:
- 遍历切片
- 如果当前值等于目标值,就返回下标
- 如果找不到,就返回
-1
现在我们想支持 []string:
func IndexString(s []string, x string) int {
for i, v := range s {
if v == x {
return i
}
}
return -1
}
你会发现,除了 int 变成 string,代码完全一样。
如果再支持 []float64 呢?
func IndexFloat64(s []float64, x float64) int {
for i, v := range s {
if v == x {
return i
}
}
return -1
}
这就是泛型出现的地方。
我们真正想表达的不是:
我要写一个 int 版本,一个 string 版本,一个 float64 版本。
而是:
我要写一个函数,它可以处理某一种类型的切片,只要这个类型能比较。
2. 泛型函数:把类型变成参数
Go 的泛型函数长这样:
func Index[T comparable](s []T, x T) int {
for i, v := range s {
if v == x {
return i
}
}
return -1
}
最关键的是这一段:
[T comparable]
它写在函数名后面、普通参数列表前面。
完整结构是:
func 函数名[类型参数 约束](普通参数) 返回值 {
函数体
}
所以:
func Index[T comparable](s []T, x T) int
可以读成:
Index有一个类型参数T,这个T必须满足comparable约束。函数接收一个[]T类型的切片s,以及一个T类型的值x,最后返回一个int。
换成人话:
给我一组同类型的值,再给我一个同类型的目标值,我帮你找它的位置。
4. comparable 是什么?
comparable 是 Go 内置的一个约束。
它表示:
这个类型的值可以使用
==和!=比较。
为什么 Index 需要 comparable?
因为函数内部写了这一句:
if v == x {
return i
}
如果一个类型不能使用 ==,这段代码就无法编译。
比如下面这些类型可以比较:
int
string
bool
float64
uintptr
指针
channel
接口值
只包含可比较字段的 struct
元素类型可比较的 array
而下面这些类型不能直接比较:
slice
map
func
包含不可比较字段的 struct
例如:
type User struct {
Name string
Tags []string
}
User 不能用 == 比较,因为它里面有一个 []string 字段,而 slice 不能比较。
所以如果你这样调用:
users := []User{
{Name: "Alice", Tags: []string{"go"}},
}
fmt.Println(Index(users, User{Name: "Alice"}))
会编译失败。
这不是泛型故意刁难你,而是 Go 在保护你:既然函数里要用 ==,那传进来的类型就必须真的支持 ==。
5. 为什么不用 any?
你可能会想,既然泛型是为了支持很多类型,那能不能这样写?
func Index[T any](s []T, x T) int {
for i, v := range s {
if v == x {
return i
}
}
return -1
}
不行。
any 表示"任何类型"。
但不是任何类型都能用 ==。
所以当约束是 any 时,编译器只允许你做所有类型都能做的事情。比如赋值、传参、返回、放进切片里,这些可以。但 ==、+、< 这类操作不一定可以。
这句话很重要:
类型参数能做什么,不取决于你希望它做什么,而取决于它的约束允许什么。
T comparable 允许比较。
T any 不保证可以比较。
所以 Index 必须用 comparable。
6. 类型推断:通常不用手写 T
泛型函数可以显式写类型参数:
Index[int]([]int{10, 20, 15}, 15)
Index[string]([]string{"foo", "bar"}, "bar")
但大多数时候不用写。
Go 可以根据普通参数推断出 T:
Index([]int{10, 20, 15}, 15)
Index([]string{"foo", "bar"}, "bar")
第一行里,s 是 []int,x 是 int,所以 T 是 int。
第二行里,s 是 []string,x 是 string,所以 T 是 string。
但是类型必须一致。
Index([]int{10, 20, 15}, "15")
这不行。
因为第一个参数说明 T 是 int,第二个参数却给了 string。
泛型不是"放弃类型检查",正好相反,它让你在复用逻辑的同时保留静态类型安全。
7. 完整运行例子:泛型 Index
package main
import "fmt"
func Index[T comparable](s []T, x T) int {
for i, v := range s {
if v == x {
return i
}
}
return -1
}
func main() {
nums := []int{10, 20, 15, -10}
fmt.Println(Index(nums, 15))
words := []string{"foo", "bar", "baz"}
fmt.Println(Index(words, "hello"))
names := []string{"Alice", "Bob", "Carol"}
fmt.Println(Index(names, "Bob"))
}
输出:
2
-1
1
这个例子虽小,但已经体现了泛型函数的价值:
一份逻辑,多个类型,编译期保证安全。
8. 泛型类型:让结构体也带类型参数
除了函数可以泛型,类型也可以泛型。
Go Tour 里的例子是:
type List[T any] struct {
next *List[T]
val T
}
这是一个单向链表节点。
先看普通版本。如果只存 int,可以写:
type IntList struct {
next *IntList
val int
}
如果只存 string,可以写:
type StringList struct {
next *StringList
val string
}
问题又来了:结构一样,逻辑一样,只是值的类型不同。
于是我们可以写成:
type List[T any] struct {
next *List[T]
val T
}
这里的:
[T any]
表示:
List是一个泛型类型,它有一个类型参数T,这个T可以是任何类型。
字段:
val T
表示节点里保存一个 T 类型的值。
字段:
next *List[T]
表示下一个节点,是同样元素类型的链表节点。
如果当前是:
List[int]
那么 next 就是:
*List[int]
如果当前是:
List[string]
那么 next 就是:
*List[string]
这样一个链表里就不会混进不一致的类型。
9. 初始化泛型类型
泛型类型使用时,要给出具体类型。
n1 := &List[int]{
val: 10,
}
n2 := &List[int]{
val: 20,
}
n1.next = n2
这里的 List[int] 表示:
一个存
int的链表。
字符串版本:
w1 := &List[string]{
val: "hello",
}
w2 := &List[string]{
val: "world",
}
w1.next = w2
这里是:
List[string]
表示:
一个存
string的链表。
你不能把 *List[string] 放到 *List[int] 的 next 里:
nums := &List[int]{val: 10}
words := &List[string]{val: "hello"}
nums.next = words // 编译错误
这就是静态类型安全。
10. 给泛型类型写方法
泛型类型也可以有方法。
比如给 List[T] 写一个 Values 方法,把链表转成切片:
func (l *List[T]) Values() []T {
var values []T
for node := l; node != nil; node = node.next {
values = append(values, node.val)
}
return values
}
注意 receiver:
func (l *List[T]) Values() []T
这里不是:
func (l *List) Values()
因为 List 是泛型类型,使用时必须带上类型参数:
List[T]
完整例子:
package main
import "fmt"
type List[T any] struct {
next *List[T]
val T
}
func (l *List[T]) Values() []T {
var values []T
for node := l; node != nil; node = node.next {
values = append(values, node.val)
}
return values
}
func main() {
nums := &List[int]{
val: 10,
next: &List[int]{
val: 20,
next: &List[int]{
val: 30,
},
},
}
fmt.Println(nums.Values())
words := &List[string]{
val: "hello",
next: &List[string]{
val: "world",
},
}
fmt.Println(words.Values())
}
输出:
[10 20 30]
[hello world]
同一个 Values 方法,既服务于 List[int],也服务于 List[string]。
11. 给链表加一个 PushFront
链表常见操作是往头部插入一个值。
我们可以写一个函数:
func PushFront[T any](head *List[T], val T) *List[T] {
return &List[T]{
val: val,
next: head,
}
}
这个函数的意思是:
给我一个
*List[T]作为旧头节点,再给我一个T类型的值,我返回一个新的头节点。
完整代码:
package main
import "fmt"
type List[T any] struct {
next *List[T]
val T
}
func PushFront[T any](head *List[T], val T) *List[T] {
return &List[T]{
val: val,
next: head,
}
}
func (l *List[T]) Values() []T {
var values []T
for node := l; node != nil; node = node.next {
values = append(values, node.val)
}
return values
}
func main() {
var nums *List[int]
nums = PushFront(nums, 30)
nums = PushFront(nums, 20)
nums = PushFront(nums, 10)
fmt.Println(nums.Values())
var words *List[string]
words = PushFront(words, "world")
words = PushFront(words, "hello")
fmt.Println(words.Values())
}
输出:
[10 20 30]
[hello world]
这里有一个很自然的点:
var nums *List[int]
这是一个空链表,头节点是 nil。
第一次插入:
nums = PushFront(nums, 30)
得到:
30
第二次插入:
nums = PushFront(nums, 20)
得到:
20 -> 30
第三次插入:
nums = PushFront(nums, 10)
得到:
10 -> 20 -> 30
所以最后转成切片就是:
[10 20 30]
12. 用构造函数创建泛型链表
如果想一次性从多个值创建链表,可以写一个构造函数:
func NewList[T any](values ...T) *List[T] {
var head *List[T]
for i := len(values) - 1; i >= 0; i-- {
head = PushFront(head, values[i])
}
return head
}
为什么从后往前遍历?
因为 PushFront 是插到头部。
如果传入:
NewList(10, 20, 30)
我们希望链表顺序是:
10 -> 20 -> 30
所以要先插 30,再插 20,最后插 10。
完整代码:
package main
import "fmt"
type List[T any] struct {
next *List[T]
val T
}
func PushFront[T any](head *List[T], val T) *List[T] {
return &List[T]{
val: val,
next: head,
}
}
func NewList[T any](values ...T) *List[T] {
var head *List[T]
for i := len(values) - 1; i >= 0; i-- {
head = PushFront(head, values[i])
}
return head
}
func (l *List[T]) Values() []T {
var values []T
for node := l; node != nil; node = node.next {
values = append(values, node.val)
}
return values
}
func main() {
nums := NewList(10, 20, 30)
fmt.Println(nums.Values())
words := NewList("go", "generic", "list")
fmt.Println(words.Values())
}
输出:
[10 20 30]
[go generic list]
你会发现这里没有写:
NewList[int](10, 20, 30)
因为 Go 可以根据参数推断出 T 是 int。
当然你也可以显式写:
nums := NewList[int](10, 20, 30)
words := NewList[string]("go", "generic", "list")
13. any:最宽松的约束
any 是 Go 里最宽松的泛型约束。
它本质上等价于:
interface{}
也就是说:
type List[T any] struct {
val T
}
和下面这种表达的意思接近:
type List[T interface{}] struct {
val T
}
只是 any 更短,也更符合泛型语境。
什么时候用 any?
当你的代码不需要对 T 做特殊操作时。
比如链表只是保存值、返回值、遍历值:
val T
[]T
append(values, node.val)
这些操作对所有类型都成立,所以 any 足够。
但如果你要比较:
v == x
那就不能用 any,要用 comparable。
如果你要做加法:
a + b
那 any 也不够,需要一个允许加法的约束。
14. 自定义约束:让泛型更精确
有时候内置的 any 和 comparable 不够。
比如你想写一个 Min 函数:
func Min[T ???](a T, b T) T {
if a < b {
return a
}
return b
}
这里需要使用 <。
但不是所有类型都能用 <。
我们可以定义一个约束:
type Ordered interface {
~int | ~int64 | ~float64 | ~string
}
然后:
func Min[T Ordered](a T, b T) T {
if a < b {
return a
}
return b
}
完整例子:
package main
import "fmt"
type Ordered interface {
~int | ~int64 | ~float64 | ~string
}
func Min[T Ordered](a T, 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"))
}
输出:
10
2.71
go
这里的 | 表示类型集合的并集:
~int | ~int64 | ~float64 | ~string
意思是:
T可以是这些类型之一。
前面的 ~ 表示允许底层类型是这个类型的自定义类型。
比如:
type Age int
Age 的底层类型是 int。
如果约束写成:
int
那 Age 不一定能匹配。
如果写成:
~int
那 Age 就可以匹配。
这就是 ~ 的作用:允许"底层类型为某类型"的自定义类型。
15. 泛型不是 interface{} 的换皮
在泛型出现之前,有些代码会用 interface{} 写通用逻辑。
例如:
func PrintAll(values []interface{}) {
for _, v := range values {
fmt.Println(v)
}
}
这看起来也能接收各种类型,但它有几个问题。
第一,[]int 不能直接当成 []interface{} 传入。
nums := []int{1, 2, 3}
PrintAll(nums) // 不行
第二,取出来以后类型信息变弱了。
func First(values []interface{}) interface{} {
return values[0]
}
调用方拿到的是 interface{},还要类型断言。
泛型可以保留类型信息:
func First[T any](values []T) T {
return values[0]
}
调用:
n := First([]int{1, 2, 3})
s := First([]string{"go", "lang"})
这里 n 自动是 int,s 自动是 string。
这就是泛型比 interface{} 更适合通用容器和通用算法的地方:
泛型复用代码,但不丢失类型。
16. 什么时候该用泛型?
泛型很好,但不是所有地方都该用。
适合用泛型的场景:
- 写通用数据结构,比如栈、队列、链表、集合。
- 写通用算法,比如
Index、Map、Filter、Reduce。 - 多个函数逻辑相同,只是类型不同。
- 你希望复用逻辑,同时保留静态类型。
不太适合用泛型的场景:
- 只有一个具体类型在用。
- 类型虽然不同,但业务含义完全不同。
- 用接口能更自然地表达行为。
- 泛型让代码变得更难读,而不是更清楚。
比如这个场景适合接口:
type Reader interface {
Read(p []byte) (n int, err error)
}
你关心的不是具体类型是什么,而是它有没有 Read 这个能力。
而这个场景适合泛型:
func First[T any](values []T) T {
return values[0]
}
你关心的是容器里的元素类型,并希望返回同样的类型。
一个简单判断是:
如果你要表达"能力",优先考虑接口。
如果你要表达"同一份逻辑作用于不同类型",考虑泛型。
17. 泛型代码里的几个常见误区
误区一:以为 any 什么都能做
func Equal[T any](a T, b T) bool {
return a == b
}
这不行。
any 表示任何类型,而任何类型并不都能比较。
应该写:
func Equal[T comparable](a T, b T) bool {
return a == b
}
误区二:类型参数越多越高级
下面这种代码看起来很通用:
func Convert[A any, B any](a A) B {
var b B
return b
}
但它几乎没有实际意义。
泛型的目标不是让函数签名变复杂,而是消除真实的重复。
误区三:把泛型当成继承
泛型不是继承,也不是"任意类型乱传"。
func Handle[T Animal](v T)
这不是说 T 继承了 Animal。
在 Go 里,约束描述的是类型集合和可用操作。泛型关注的是:
这个类型参数允许是什么,以及在函数内部能做什么。
误区四:忘了 nil 链表
比如:
func (l *List[T]) Values() []T {
var values []T
for node := l; node != nil; node = node.next {
values = append(values, node.val)
}
return values
}
这个方法可以处理 nil receiver:
var nums *List[int]
fmt.Println(nums.Values())
输出:
[]
因为循环一开始 node := l,如果 l 是 nil,循环不会进入。
这是链表方法里很常见也很舒服的写法。
18. 泛型的美感:少写重复,但不牺牲清楚
Go 的泛型不是为了让代码看起来更抽象。
它更像是一种克制的工具:
当你发现自己在写三份几乎一样的代码,只是 int、string、float64 不同,它就出现了。
Index[T comparable] 说的是:
我不关心具体是
int还是string,我只要求它能比较。
List[T any] 说的是:
我不关心具体存什么,我只是负责把这些值串起来。
泛型真正好的地方,不是把所有类型混在一起,而是在保留类型安全的前提下,让重复的逻辑合并成一份。
它让代码更少,但不让类型更模糊。
它让抽象出现,但仍然站在编译器能检查的地面上。
结语
你可以这样记 Go 泛型:
func Index[T comparable](s []T, x T) int
这里的 T 是类型参数,comparable 是约束。
type List[T any] struct {
next *List[T]
val T
}
这里的 List[T] 是泛型类型,T 决定链表里存什么。
any 表示什么类型都可以,但能做的操作很少。
comparable 表示可以用 == 和 !=。
自定义约束可以表达更精确的类型集合。
类型推断让大多数泛型调用看起来和普通函数一样自然。
泛型不是 Go 里最日常的语法,但一旦你开始写通用容器和通用算法,它会变得非常顺手。
它像一把轻一点的刀:不炫目,但锋利;不强迫你抽象,但在重复出现时,给你一条干净的路。