引入

写一个 Min 函数,int 写一遍,float64 又写一遍,string 还要写一遍。Go 1.18 引入泛型,用类型参数让一个函数或类型支持多种类型,不用再复制粘贴了。

正文

定义

定义

泛型(generics)通过类型参数(type parameter)让函数和类型支持多种类型,同时保持类型安全。类型参数用方括号 [] 声明,后面跟类型约束(constraint)限制允许的类型范围。

语法

// 泛型函数
func Min[T constraints.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}
 
// 泛型类型
type Set[T comparable] struct {
    items map[T]bool
}

例子

泛型函数

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]
}

Map 函数接收两种类型参数 TU,编译器根据调用时的参数自动推断类型。

类型约束

import "golang.org/x/exp/constraints"
 
func Sum[T constraints.Integer](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

constraints.Integer 约束 T 必须是整数类型,constraints.Ordered 约束 T 必须支持比较运算符。

自定义约束

type Number interface {
    ~int | ~float64
}
 
func Abs[T Number](n T) T {
    if n < 0 {
        return -n
    }
    return n
}

用接口定义类型集合~int 表示底层类型是 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 := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return last, true
}

泛型结构体让数据结构一次定义,多次复用,不用为每种类型各写一份。

类型推断

func main() {
    // 显式指定类型参数
    fmt.Println(Min[int](3, 5))
 
    // 自动推断(大多数情况可以省略)
    fmt.Println(Min(3, 5))
    fmt.Println(Min("apple", "banana"))
}

Go 编译器能从函数参数自动推断类型参数,不用每次手动写。

常见写法

写法说明
func F[T any](…)泛型函数,T 可以是任意类型
func F[T comparable](…)T 必须可比较(支持 ==)
func F[T constraints.Ordered](…)T 必须支持排序比较
type S[T any] struct{}泛型结构体
~int底层类型是 int 的所有类型
type C interface{ ~int | ~string }自定义类型约束
F[int](args)显式指定类型参数
F(args)编译器自动推断类型参数

特点

  • 类型参数用方括号 [T constraint] 声明
  • anyinterface{} 的别名,comparable 要求类型支持 ==
  • ~ 前缀表示底层类型匹配,包括自定义类型
  • 编译器能自动推断类型参数,多数时候不用显式写
  • 泛型是编译时检查,不是运行时反射,性能不受影响
  • constraints 包提供 OrderedIntegerFloat 等常用约束
  • 不要滥用泛型,只在该函数需要支持多种类型时才用

理解

泛型就是给函数和类型加了一个”类型占位符”。编译器在编译时把它替换成具体类型,类型安全和性能都不打折。配合 constraints 包和自定义类型集合,能写出干净复用的通用代码,不用再给每种类型抄一遍了。

引出

泛型在编译时处理类型,但有时候需要在运行时动态获取和操作类型信息。接下来看 Go 反射,学习 reflect 包怎么在运行时检查类型和值。