Golang 泛型
Golang 泛型详解
Go 从 1.18 开始正式支持泛型。泛型的核心目标不是把 Go 变成 Java / C++ 那种重泛型语言,而是在保持 Go 简洁性的前提下,解决一类很常见的问题:同一套逻辑因为类型不同被迫重复写多份。
典型例子:
func SumInts(nums []int) int {
var sum int
for _, n := range nums {
sum += n
}
return sum
}
func SumFloat64s(nums []float64) float64 {
var sum float64
for _, n := range nums {
sum += n
}
return sum
}
这两个函数逻辑完全一样,只是元素类型不同。泛型可以把类型抽象成参数,让一份代码适配多个类型:
func Sum[T int | float64](nums []T) T {
var sum T
for _, n := range nums {
sum += n
}
return sum
}
调用:
fmt.Println(Sum([]int{1, 2, 3}))
fmt.Println(Sum([]float64{1.1, 2.2, 3.3}))
核心概念
Go 泛型里最重要的几个概念:
- 类型参数:函数或类型声明里的
[T any]、[K comparable, V any]。 - 类型实参:调用泛型函数或使用泛型类型时传入的具体类型,例如
Sum[int]中的int。 - 类型约束:限制类型参数能接受哪些类型,以及这些类型支持哪些操作。
- 实例化:编译器用具体类型替换类型参数,得到可使用的普通函数或普通类型。
- 类型推断:调用泛型函数时,如果编译器能从参数推断出类型实参,就可以省略
[int]这类显式写法。
泛型函数
基本语法
泛型函数的类型参数写在函数名后面、普通参数列表前面:
func 函数名[类型参数列表](普通参数列表) 返回值 {
// 函数体
}
例子:
func Identity[T any](v T) T {
return v
}
T 是类型参数,any 是约束。any 是 interface{} 的别名,表示允许任意非接口类型作为类型实参。
调用时可以显式指定类型:
name := Identity[string]("Tom")
age := Identity[int](18)
也可以让编译器推断:
name := Identity("Tom") // T 推断为 string
age := Identity(18) // T 推断为 int
多个类型参数
func Map[T any, R any](items []T, fn func(T) R) []R {
result := make([]R, 0, len(items))
for _, item := range items {
result = append(result, fn(item))
}
return result
}
调用:
nums := []int{1, 2, 3}
texts := Map(nums, func(n int) string {
return fmt.Sprintf("num=%d", n)
})
fmt.Println(texts) // [num=1 num=2 num=3]
这里有两个类型参数:
T:输入切片元素类型。R:输出切片元素类型。
编译器可以根据 nums 和匿名函数推断出 T=int、R=string。
相同约束的简写
如果多个类型参数约束相同,可以写在一起:
func Pair[A, B any](a A, b B) (A, B) {
return a, b
}
等价于:
func Pair[A any, B any](a A, b B) (A, B) {
return a, b
}
类型参数可以用于参数、返回值和局部变量
func First[T any](items []T) (T, bool) {
var zero T
if len(items) == 0 {
return zero, false
}
return items[0], true
}
注意 var zero T 是泛型代码里常见写法,用于得到 T 类型的零值。
泛型类型
泛型不仅能用于函数,也能用于类型定义,例如结构体、切片别名、map 别名等。
泛型结构体
type Pair[T any, U any] struct {
First T
Second U
}
使用时必须实例化:
p := Pair[string, int]{
First: "age",
Second: 18,
}
泛型类型不能直接裸用:
var p Pair // 编译错误:generic type Pair requires instantiation
必须写成:
var p Pair[string, int]
泛型方法
泛型类型可以定义方法,方法接收器要带上类型参数:
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
last := len(s.items) - 1
v := s.items[last]
s.items = s.items[:last]
return v, true
}
使用:
var s Stack[int]
s.Push(10)
s.Push(20)
v, ok := s.Pop()
fmt.Println(v, ok) // 20 true
注意:Go 的方法不能声明自己独立的类型参数。
type Box[T any] struct {
value T
}
// 编译错误:method must have no type parameters
func (b Box[T]) Map[U any](fn func(T) U) U {
return fn(b.value)
}
如果需要新的类型参数,通常改成普通泛型函数:
func MapBox[T any, U any](b Box[T], fn func(T) U) U {
return fn(b.value)
}
泛型类型别名
Go 1.24 开始,类型别名也可以带类型参数:
type Set[T comparable] = map[T]bool
如果项目需要兼容 Go 1.23 或更早版本,不要使用泛型类型别名,可以改成类型定义:
type Set[T comparable] map[T]bool
类型约束
类型约束决定了类型参数能接收什么类型,以及泛型代码里能对这个值做什么操作。
any
any 是最宽泛的约束:
func Print[T any](v T) {
fmt.Println(v)
}
any 表示不限制具体类型,但也意味着你不能随便对 T 做特定运算。
下面代码无法编译:
func Add[T any](a, b T) T {
return a + b // 编译错误:T 不一定支持 +
}
如果要用 +,约束里必须明确这些类型都支持 +。
联合类型
用 | 表示类型集合的并集:
func Add[T int | int64 | float64](a, b T) T {
return a + b
}
这里 T 只能是 int、int64 或 float64。
可以把约束提取成接口:
type Number interface {
int | int64 | float64
}
func Add[T Number](a, b T) T {
return a + b
}
这种接口主要用于类型约束,不是传统意义上“给变量赋值用”的接口。
~ 底层类型约束
如果写:
type Number interface {
int | int64
}
那么下面的自定义类型不能传进去:
type MyInt int
Add(MyInt(1), MyInt(2)) // 编译错误
因为 MyInt 是一个新的命名类型,不是 int 本身。
如果希望允许所有底层类型是 int 的命名类型,就使用 ~int:
type Integer interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
func AddInt[T Integer](a, b T) T {
return a + b
}
调用:
type MyInt int
fmt.Println(AddInt(MyInt(1), MyInt(2))) // 3
~T 的意思是:允许底层类型为 T 的所有类型。
comparable
comparable 是 Go 预声明的约束,表示该类型的值可以使用 == 和 !=。
它最常见的场景是 map key:
func Keys[K comparable, V any](m map[K]V) []K {
keys := make([]K, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
为什么 K 必须是 comparable?
因为 Go 的 map key 必须可比较。string、int、指针、结构体等部分类型可以做 key;slice、map、function 不可以。
Keys(map[string]int{"a": 1}) // 可以
// 编译错误:[]int 不能作为 map key
// Keys(map[[]int]string{})
Ordered 有序约束
标准库 cmp 包提供了 cmp.Ordered 约束,适合需要 <、>、<=、>= 的场景。
import "cmp"
func Min[T cmp.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
如果项目需要兼容 Go 1.20 或更低版本,可以自己定义:
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}
func Min[T Ordered](a, b T) T {
if a < b {
return a
}
return b
}
约束里同时包含方法和类型集合
约束接口既可以写类型集合,也可以写方法。
type StringNumber interface {
~int | ~int64
String() string
}
这个约束的含义是:
- 类型的底层类型必须是
int或int64。 - 类型还必须有
String() string方法。
例子:
type UserID int64
func (id UserID) String() string {
return fmt.Sprintf("user:%d", id)
}
func PrintID[T StringNumber](id T) {
fmt.Println(id.String())
}
非基本接口只能做约束
包含类型集合的接口叫做非基本接口,只能用作类型约束,不能作为普通变量类型使用。
type Number interface {
~int | ~float64
}
// 编译错误:不能把包含类型约束的接口当普通变量类型使用
// var n Number
普通接口可以当变量类型:
type Reader interface {
Read([]byte) (int, error)
}
var r Reader
简单记:
- 只有方法的接口:可以当普通接口用。
- 包含
~int、int | string这类类型项的接口:主要给泛型约束用。
类型推断
泛型函数调用时,类型实参可以显式写,也可以由编译器推断。
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
Max[int](1, 2) // 显式指定 T=int
Max(1, 2) // 编译器推断 T=int
根据普通参数推断
func Contains[T comparable](items []T, target T) bool {
for _, item := range items {
if item == target {
return true
}
}
return false
}
Contains([]string{"a", "b"}, "a") // T 推断为 string
Contains([]int{1, 2}, 2) // T 推断为 int
无法仅根据返回值推断
func Zero[T any]() T {
var zero T
return zero
}
// 编译错误:无法推断 T
// v := Zero()
v := Zero[int]()
fmt.Println(v) // 0
Go 的类型推断主要基于函数参数和上下文,但不要期待它像某些语言一样总能根据返回值推断。
无类型常量推断
func Double[T ~int | ~float64](v T) T {
return v + v
}
fmt.Println(Double(10)) // T 推断为 int
fmt.Println(Double(1.5)) // T 推断为 float64
无类型整数常量默认倾向于 int,无类型浮点常量默认倾向于 float64。
常见使用场景
1. 集合工具函数
泛型最适合写 slice / map 这类集合工具。
Filter
func Filter[T any](items []T, keep func(T) bool) []T {
result := make([]T, 0, len(items))
for _, item := range items {
if keep(item) {
result = append(result, item)
}
}
return result
}
使用:
nums := []int{1, 2, 3, 4, 5}
evens := Filter(nums, func(n int) bool {
return n%2 == 0
})
fmt.Println(evens) // [2 4]
Map
func Map[T any, R any](items []T, fn func(T) R) []R {
result := make([]R, 0, len(items))
for _, item := range items {
result = append(result, fn(item))
}
return result
}
Reduce
func Reduce[T any, R any](items []T, init R, fn func(R, T) R) R {
acc := init
for _, item := range items {
acc = fn(acc, item)
}
return acc
}
2. 栈、队列、集合
Stack
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
last := len(s.items) - 1
v := s.items[last]
s.items = s.items[:last]
return v, true
}
Set
type Set[T comparable] map[T]struct{}
func NewSet[T comparable](items ...T) Set[T] {
s := make(Set[T], len(items))
for _, item := range items {
s[item] = struct{}{}
}
return s
}
func (s Set[T]) Add(v T) {
s[v] = struct{}{}
}
func (s Set[T]) Has(v T) bool {
_, ok := s[v]
return ok
}
使用:
roles := NewSet("admin", "user")
fmt.Println(roles.Has("admin")) // true
3. 类型安全的缓存
不用泛型时,经常会写成 map[string]any,取值时再做类型断言:
type Cache struct {
data map[string]any
}
这种写法的问题是取出来的值不安全。
使用泛型:
type Cache[K comparable, V any] struct {
data map[K]V
}
func NewCache[K comparable, V any]() *Cache[K, V] {
return &Cache[K, V]{
data: make(map[K]V),
}
}
func (c *Cache[K, V]) Set(k K, v V) {
c.data[k] = v
}
func (c *Cache[K, V]) Get(k K) (V, bool) {
v, ok := c.data[k]
return v, ok
}
使用:
userCache := NewCache[string, User]()
userCache.Set("u1", User{Name: "Tom"})
user, ok := userCache.Get("u1")
fmt.Println(user.Name, ok)
4. 保留命名切片类型
下面这个例子非常能体现 ~ 的价值。
type Ints []int
func Clone[S ~[]E, E any](s S) S {
return append(s[:0:0], s...)
}
使用:
var a Ints = []int{1, 2, 3}
b := Clone(a)
fmt.Printf("%T\n", b) // main.Ints
如果函数签名写成:
func Clone[E any](s []E) []E
传入 Ints 后返回值会退化成 []int,无法保留原来的命名类型。
5. 类型安全的 Option / Result
Go 没有内置 Option / Result,但泛型可以表达这类容器。
type Option[T any] struct {
value T
ok bool
}
func Some[T any](v T) Option[T] {
return Option[T]{value: v, ok: true}
}
func None[T any]() Option[T] {
return Option[T]{}
}
func (o Option[T]) Value() (T, bool) {
return o.value, o.ok
}
使用:
func FindUser(id int64) Option[User] {
if id <= 0 {
return None[User]()
}
return Some(User{ID: id})
}
是否采用这种风格要看团队习惯。Go 里 value, ok 和 value, error 仍然是最主流的表达方式。
注意事项
1. 不要为了泛型而泛型
下面这种代码没有必要泛型化:
func PrintUser(u User) {
fmt.Println(u.Name)
}
如果只有一种明确类型,就直接写具体类型。泛型适合多个类型共享同一套逻辑的场景。
2. 泛型不能替代接口
接口解决的是“行为抽象”,泛型解决的是“类型参数化”。
接口例子:
type Notifier interface {
Notify(ctx context.Context, msg string) error
}
func Send(ctx context.Context, n Notifier, msg string) error {
return n.Notify(ctx, msg)
}
这里关心的是对象有没有 Notify 行为,用接口更自然。
泛型例子:
func Reverse[S ~[]E, E any](s S) S {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
return s
}
这里关心的是“同一套算法适配不同元素类型”,用泛型更自然。
3. 约束决定可用操作
泛型函数体里能做什么,不取决于调用时实际传入了什么类型,而取决于约束里承诺了什么能力。
func Equal[T any](a, b T) bool {
return a == b // 编译错误:T 不一定可比较
}
应该写成:
func Equal[T comparable](a, b T) bool {
return a == b
}
4. 不能直接访问字段
即使约束中的所有类型都有同名字段,Go 也不能通过类型参数直接访问字段。
type User struct {
Name string
}
type Admin struct {
Name string
}
type HasName interface {
User | Admin
}
func PrintName[T HasName](v T) {
// 编译错误:v.Name undefined
// fmt.Println(v.Name)
}
推荐改成方法约束:
type Named interface {
Name() string
}
func PrintName[T Named](v T) {
fmt.Println(v.Name())
}
5. 类型断言和类型 switch 要转成 any
类型参数本身不是接口值,不能直接做类型 switch:
func PrintType[T any](v T) {
// 编译错误
// switch v.(type) {
// }
}
可以先转成 any:
func PrintType[T any](v T) {
switch any(v).(type) {
case int:
fmt.Println("int")
case string:
fmt.Println("string")
default:
fmt.Println("other")
}
}
但要注意,这检查的是运行时值的实际动态类型,不是泛型约束本身。
6. nil 和类型参数
不要随便返回 nil 给 T:
func Zero[T any]() T {
return nil // 编译错误:T 不一定能赋 nil
}
正确做法:
func Zero[T any]() T {
var zero T
return zero
}
如果约束确保 T 一定是指针、slice、map、chan、func 或 interface,才可以考虑使用 nil。
7. new(T) 可以,T{} 有条件
new(T) 可以用:
func NewValue[T any]() *T {
return new(T)
}
T{} 只有在约束保证所有类型都有相同的底层复合类型时才可用。一般业务里不建议依赖这种写法,直接使用 var zero T 更稳。
8. 不要把约束设计得太宽
约束越宽,函数体能做的操作越少。
func BadAdd[T any](a, b T) T {
return a + b // 不行
}
约束应该刚好表达函数需要的能力:
type Addable interface {
~int | ~int64 | ~float64 | ~string
}
func Add[T Addable](a, b T) T {
return a + b
}
9. 不要把约束设计得太窄
func Sum[T int | int64](nums []T) T {
var sum T
for _, n := range nums {
sum += n
}
return sum
}
这个函数不支持自定义类型:
type Score int
如果希望支持 Score,应该写成:
func Sum[T ~int | ~int64](nums []T) T {
var sum T
for _, n := range nums {
sum += n
}
return sum
}
泛型和 interface{} 的区别
interface{} 写法
func First(items []any) any {
if len(items) == 0 {
return nil
}
return items[0]
}
问题:
- 调用方需要类型断言。
- 编译期不能保证元素类型一致。
- 容易把错误推迟到运行时。
v := First([]any{1, "a", true})
n := v.(int)
fmt.Println(n)
泛型写法
func First[T any](items []T) (T, bool) {
var zero T
if len(items) == 0 {
return zero, false
}
return items[0], true
}
优点:
- 返回值类型明确。
- 编译期检查类型。
- 不需要类型断言。
n, ok := First([]int{1, 2, 3})
fmt.Println(n, ok)
泛型和反射的区别
反射适合处理运行时才知道类型的场景,例如 JSON 编解码、ORM、通用序列化框架。
泛型适合处理编译期已经知道类型,只是希望复用算法的场景。
反射版本:
func IsZeroByReflect(v any) bool {
return reflect.ValueOf(v).IsZero()
}
泛型版本:
func IsZero[T comparable](v T) bool {
var zero T
return v == zero
}
对比:
- 泛型有编译期类型检查。
- 泛型通常更容易被优化。
- 反射更灵活,但更容易写出运行时错误。
原理分析
语言层面的原理
从语言规范角度看,泛型的核心是“类型参数 + 类型集合”。
func Sum[T ~int | ~float64](nums []T) T
可以理解为:
T是一个待确定的类型。T必须属于~int | ~float64这个类型集合。- 函数体里只能使用这个类型集合共同支持的操作。
- 调用时,编译器用具体类型替换
T。
例如:
Sum([]int{1, 2})
实例化时可以理解为:
func SumInt(nums []int) int {
var sum int
for _, n := range nums {
sum += n
}
return sum
}
这只是帮助理解。实际编译器如何生成代码属于实现细节,不是语言规范承诺。
编译器实现思路
Go 官方 gc 编译器在 Go 1.18 的泛型实现中采用了字典和 gcshape stenciling 的组合思路。
简单理解:
- 编译器会为泛型函数创建可执行的实例化版本。
- 不一定为每一个具体类型都生成一份完全独立的机器码。
- 一些底层表示相同或相近的类型可以共享同一份 shape 实例。
- 对于需要运行时类型信息、方法调用、接口转换等情况,会通过字典传递必要信息。
例如下面的泛型函数:
func PrintAll[T fmt.Stringer](items []T) {
for _, item := range items {
fmt.Println(item.String())
}
}
调用 item.String() 时,编译器需要知道具体类型如何调用这个方法。泛型实例化后,相关方法信息可能通过字典或实例化后的代码路径来完成。
对开发者来说,最重要的不是记住编译器细节,而是理解两个结论:
- 泛型是编译期类型机制,不是运行时动态类型系统。
- 不要依赖某个具体版本编译器生成代码的方式来写业务逻辑。
性能分析
泛型通常比 interface{} + 类型断言更类型安全,也更容易被编译器优化。但它不是“必然更快”的银弹。
影响性能的因素包括:
- 是否发生接口转换。
- 是否能内联。
- 是否引入额外分配。
- 约束是否涉及方法调用。
- 实际编译器版本的优化能力。
例如纯数值计算:
func Sum[T ~int | ~int64 | ~float64](nums []T) T {
var sum T
for _, n := range nums {
sum += n
}
return sum
}
这种通常很适合泛型。
如果约束是接口方法:
type Encoder interface {
Encode() []byte
}
func EncodeAll[T Encoder](items []T) [][]byte {
result := make([][]byte, 0, len(items))
for _, item := range items {
result = append(result, item.Encode())
}
return result
}
它的性能表现要结合方法调用、逃逸分析、内联结果来看。真正关心性能时,应使用 benchmark 验证。
设计建议
优先从具体代码开始
不要一开始就设计复杂泛型抽象。更推荐:
- 先写具体类型版本。
- 发现第二个、第三个类型有相同逻辑。
- 再抽成泛型。
约束要面向操作
约束不是为了列举“我想到的所有类型”,而是为了表达“函数体需要什么能力”。
如果函数只需要比较相等:
func IndexOf[T comparable](items []T, target T) int
如果函数需要排序:
func Sort[T cmp.Ordered](items []T)
如果函数需要调用方法:
type Validator interface {
Validate() error
}
func ValidateAll[T Validator](items []T) error
公共 API 谨慎使用复杂约束
库对外暴露的泛型 API 应该尽量简单。
不太推荐:
type Weird[T interface {
~[]E | ~map[string]E
Custom()
}, E any] struct {
}
这种 API 读起来很费劲,调用方也不容易理解错误信息。
更推荐把复杂逻辑拆开,用清晰的类型、函数和接口组合表达。
常见错误汇总
1. 忘记实例化泛型类型
type Box[T any] struct {
Value T
}
// var b Box // 错误
var b Box[int]
2. 使用了约束没有承诺的操作
func Less[T any](a, b T) bool {
return a < b // 错误
}
应该写成:
func Less[T cmp.Ordered](a, b T) bool {
return a < b
}
3. 把约束接口当普通接口用
type Number interface {
~int | ~float64
}
// var n Number // 错误
4. 方法声明额外类型参数
type List[T any] struct{}
// func (l List[T]) Convert[U any]() []U { return nil } // 错误
改成函数:
func ConvertList[T any, U any](l List[T], fn func(T) U) []U {
return nil
}
5. 类型推断失败
func Make[T any]() []T {
return nil
}
// xs := Make() // 错误:无法推断 T
xs := Make[int]()
什么时候应该使用泛型
适合使用:
- slice / map / set / stack / queue 等容器。
- map、filter、reduce、clone、reverse 等集合算法。
- cache、pool、optional、result 等类型安全容器。
- 数值算法,例如 sum、min、max。
- 避免
interface{}+ 类型断言导致的运行时错误。 - 希望保留命名类型,例如
S ~[]E这种场景。
不适合使用:
- 只有一个具体类型的业务函数。
- 为了炫技而抽象出复杂约束。
- 本来用接口表达行为更自然的场景。
- 需要运行时动态发现字段、标签、方法的场景,这类更适合反射。
- 会让错误信息和 API 变得难懂的公共接口。
完整示例:分页结果
这是业务代码里比较自然的泛型使用场景。
type Page[T any] struct {
Items []T `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
Size int `json:"size"`
}
func NewPage[T any](items []T, total int64, page, size int) Page[T] {
return Page[T]{
Items: items,
Total: total,
Page: page,
Size: size,
}
}
使用:
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
users := []User{
{ID: 1, Name: "Tom"},
{ID: 2, Name: "Jerry"},
}
page := NewPage(users, 100, 1, 20)
好处:
Page[User]、Page[Order]、Page[Product]可以复用同一个分页结构。Items保持强类型,不需要[]any。- JSON 输出结构一致。
完整示例:泛型 Repository 辅助函数
泛型不一定要替代整个 Repository 接口,但可以写一些类型安全的辅助函数。
type Entity interface {
GetID() int64
}
func IndexByID[T Entity](items []T) map[int64]T {
result := make(map[int64]T, len(items))
for _, item := range items {
result[item.GetID()] = item
}
return result
}
业务类型:
type User struct {
ID int64
Name string
}
func (u User) GetID() int64 {
return u.ID
}
使用:
users := []User{
{ID: 1, Name: "Tom"},
{ID: 2, Name: "Jerry"},
}
userMap := IndexByID(users)
fmt.Println(userMap[1].Name)
这里用方法约束比字段约束更符合 Go 的泛型能力,因为类型参数不能直接访问字段。
和 Java 泛型的区别
如果你熟悉 Java,需要注意 Go 泛型有几个明显不同:
- Go 没有
? extends T/? super T这种通配符语法。 - Go 没有泛型类继承体系。
- Go 的约束是接口 + 类型集合。
- Go 不支持方法声明独立类型参数。
- Go 的泛型更强调简单、显式、少抽象。
Java 里常见的 PECS 思路在 Go 里通常不需要照搬。Go 更推荐通过函数、接口、类型约束把 API 设计得直接一些。

浙公网安备 33010602011771号