泛型基础
本教程共 80 篇 · 第 47 篇 · 更新于 2026-07-27 · 约 7 分钟阅读
47. 泛型基础
本节目标:搞懂泛型解决什么问题,学会写泛型函数,理解类型参数和类型约束的基本概念。
为什么需要泛型
假设你要写一个「求和」函数。整数求和写一个,浮点数求和再写一个:
func SumInts(s []int) int {
var total int
for _, v := range s {
total += v
}
return total
}
func SumFloats(s []float64) float64 {
var total float64
for _, v := range s {
total += v
}
return total
}
逻辑完全一样,只是类型不同。如果有十种数字类型,你要写十个函数?
泛型就是解决这个问题的。它让你写一份代码,适用于多种类型。Go 在 1.18 版本(2022 年 3 月)引入了泛型。
泛型函数
用类型参数把上面的两个函数合成一个:
package main
import "fmt"
// [T int | float64] 是类型参数声明
func Sum[T int | float64](s []T) T {
var total T
for _, v := range s {
total += v
}
return total
}
func main() {
ints := []int{1, 2, 3}
floats := []float64{1.1, 2.2, 3.3}
fmt.Println(Sum(ints)) // 6
fmt.Println(Sum(floats)) // 6.6
}
[T int | float64] 里的 T 是类型参数,int | float64 是类型约束,表示 T 可以是 int 或 float64。
三个核心概念
- 类型参数(Type Parameter):
T,一个占位符,代表「某种类型」 - 类型约束(Type Constraint):
int | float64,限定 T 能是哪些类型 - 类型实参(Type Argument):调用时传入的具体类型
调用方式
显式指定类型
Sum[int]([]int{1, 2, 3}) // 手动指定 T = int
Sum[float64]([]float64{1.1}) // 手动指定 T = float64
类型推断(推荐)
Go 编译器能根据参数自动推断类型:
Sum([]int{1, 2, 3}) // 推断出 T = int
Sum([]float64{1.1, 2.2}) // 推断出 T = float64
Tip大部分情况下不需要手动写类型实参,让编译器推断就行。只有推断不出来时才显式指定。
any 约束
如果类型参数可以是任何类型,用 any 约束:
func Print[T any](v T) {
fmt.Println(v)
}
Print(42)
Print("hello")
Print(true)
any 是 Go 1.18 引入的 interface{} 的别名。用 any 做约束表示「不限制类型」。
comparable 约束
如果需要在泛型函数里用 == 比较,用 comparable 约束:
func Contains[T comparable](s []T, v T) bool {
for _, item := range s {
if item == v {
return true
}
}
return false
}
func main() {
fmt.Println(Contains([]int{1, 2, 3}, 2)) // true
fmt.Println(Contains([]string{"a", "b"}, "c")) // false
}
comparable 表示「可比较的类型」,包括数字、字符串、布尔、指针、channel 等。切片、map、函数不可比较,不能用 comparable。
Note
comparable和any是 Go 预声明的两个最常用约束。any不限制类型,comparable要求类型支持==和!=。
一个实用的例子:泛型 Map 函数
用泛型写一个「对切片每个元素做转换」的函数:
package main
import "fmt"
// Map 把 []T 转成 []U
func Map[T any, U any](s []T, f func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = f(v)
}
return result
}
func main() {
nums := []int{1, 2, 3}
// 把每个整数转成字符串
strs := Map(nums, func(n int) string {
return fmt.Sprintf("第%d个", n)
})
fmt.Println(strs) // [第1个 第2个 第3个]
// 把每个整数翻倍
doubled := Map(nums, func(n int) int {
return n * 2
})
fmt.Println(doubled) // [2 4 6]
}
这个函数有两个类型参数:T 是输入类型,U 是输出类型。没有泛型的话,你要为每种类型组合写一个函数。
泛型 vs 接口
你可能会问:用 any 接口不也能做到吗?
// 用接口的写法
func SumAny(s []any) any {
var total int
for _, v := range s {
total += v.(int) // 运行时类型断言,可能 panic
}
return total
}
问题在于:
- 类型安全:接口版需要在运行时做类型断言,传错类型会 panic。泛型版在编译期就检查类型,传错类型编译不过
- 性能:接口版有装箱拆箱开销,泛型版直接操作具体类型
- 可读性:泛型版的返回值是具体类型,不用再断言
// 泛型版:返回 int,直接用
result := Sum([]int{1, 2, 3}) // result 是 int
// 接口版:返回 any,还得断言
result := SumAny([]any{1, 2, 3}) // result 是 any
n := result.(int) // 还得断言
Tip能用泛型就用泛型,比接口更安全更高效。但泛型不能替代所有场景:如果逻辑需要根据不同类型做不同操作,还是得用接口。
小结
泛型基础记住这几点:
- Go 1.18 引入泛型,语法是
func Name[T Constraint](args) returns any约束不限制类型,comparable要求可比较- 调用时通常靠类型推断,不用手写类型实参
- 泛型比接口更安全(编译期检查)、更高效(无装箱)
下一章讲类型约束的深入用法:自定义约束、~ 底层类型、union 联合。