为什么要使用泛型?

Go 泛型的核心价值:

  1. 减少重复代码:同一逻辑支持多种类型,避免为不同类型写相同函数。
  2. 类型安全:替换 interface{},编译时检查类型,避免运行时错误。
  3. 性能优化:比反射或 interface{} 更高效,无运行时开销。
  4. 标准库增强:支持通用工具(如 slicesmaps 包)

1. 基本概念

1.1 函数的形参(parameter)和实参(argument)

func Add(a int, b int) int {
    // 变量a, b是函数的形参
    return a + b
}

Add(1,2) // 调用函数,传入1,2为实参

1.2 类型形参(type parameter)和类型实参(type argument)

func Add(a T, b T) T {
    return a + b
}

Add[T=int](1,2)
Add[T=string]("Hello","World)
  • [T=int] 中int 是类型实参,T表示类型形参

1.3 类型约束

type Slice[T int|float32|float64] []T

Slice[T]表示泛型类型。

int|float32|float64表示类型约束,|表示类型形参只可以接收int,float32,float64类型。

[T int|float32|float64]表示类型形参列表。

1.4 实例化(Instantiations)

泛型类型不能直接使用,必须传入类型实参,传入类型实参确定具体类型的操作被称为实例化

var s Slice[int] = []int{1,2,3}
var b Slice[float32] = []float32{1.0, 2.0, 3.0}

根据传入类型实参,可以将Slice[T]实例化Slice[int]或者Slice[float32]。

// MyMap类型定义了两个类型形参 KEY 和 VALUE。分别为两个形参指定了不同的类型约束
// 这个泛型类型的名字叫: MyMap[KEY, VALUE]
type MyMap[KEY int | string, VALUE float32 | float64] map[KEY]VALUE  
// 用类型实参 string 和 flaot64 替换了类型形参 KEY 、 VALUE,泛型类型被实例化为具体的类型:MyMap[string, float64]
var a MyMap[string, float64] = map[string]float64{
    "jack_score": 9.6,
    "bob_score":  8.4,
}
  • KEY和VALUE表示类型形参
  • int|string是KEY的类型约束,float32|float64是VALUE的类型约束
  • Map[KEY, VALUE] 是泛型类型,类型的名字就叫 Map[KEY, VALUE]
  • var a MyMap[string, float64] = xx 中的string和float64是类型实参,用于分别替换KEY和VALUE,实例化出了具体的类型 MyMap[string, float64]

1.5 泛型receiver

type MySlice[T int | float32] []T

func (s MySlice[T]) Sum() T {
    var sum T
    for _, value := range s {
        sum += value
    }
    return sum
}

var s MySlice[int] = []int{1, 2, 3, 4}
fmt.Println(s.Sum()) // 输出:10

1.6 泛型函数

func Add[T int | float32 | float64](a T, b T) T {
    return a + b
}
Add[int](1,2) // 传入类型实参int,计算结果为 3
Add[float32](1.0, 2.0) // 传入类型实参float32, 计算结果为 3.0

1.7 匿名函数不支持泛型

匿名函数不能自己定义类型形参,可以使用已经定义好的类型形参

func MyFunc[T int | float32 | float64](a, b T) {

    // 匿名函数可使用已经定义好的类型形参
    fn2 := func(i T, j T) T {
        return i*2 - j*2
    }

    fn2(a, b)
}

1.8 不支持泛型方法

type A struct {
}

// 不支持泛型方法
func (receiver A) Add[T int | float32 | float64](a T, b T) T {
    return a + b
}

Facilitators模式

通过recevicer使用类型形参来到达使用近似泛型方法的目的

  • 示例一
type Client struct{ ... }

type Querier[T any] struct {
    client *Client
}

func NewQuerier[T any](c *Client) *Querier[T] {
    return &Querier[T]{
        client: c,
    }
}

func (q *Querier[T]) All(ctx context.Context) ([]T, error) {
    // implementation
}

func (q *Querier[T]) Filter(ctx context.Context, filter ...Filter) ([]T, error) {
    // implementation
}
  • 示例二
type A[T int | float32 | float64] struct {
}

// 方法可以使用类型定义中的形参 T 
func (receiver A[T]) Add(a T, b T) T {
    return a + b
}

// 用法:
var a A[int]
a.Add(1, 2)

var aa A[float32]
aa.Add(1.0, 2.0)

1.9 类型形参不能单独使用

// 错误,类型形参不能单独使用
type CommonType[T int|string|float32] T

go将类型形参T本身定义一个类型在Go语言是不允许的

类型别名必须是一个具体的类型,即使用到了泛型,也要有明确的结构或者组合形式

定义一个泛型结构体

type CommonType[T int|string|float32] struct {
    Value T
}

定义一个泛型切片类型

type CommonSlice[T int|string|float32] []T

1.10 动态判断变量的类型

使用接口可以通过类型断言或者Switch语句来进行判断,对于value T通过形参定义的变量,不能使用Switch语句或者类型断言,可以使用反射机制进行泛型类型判断。

func (receiver Queue[T]) Put(value T) {
    // Printf() 可输出变量value的类型(底层就是通过反射实现的)
    fmt.Printf("%T", value) 

    // 通过反射可以动态获得变量value的类型从而分情况处理
    v := reflect.ValueOf(value)

    switch v.Kind() {
    case reflect.Int:
        // do something
    case reflect.String:
        // do something
    }

    // ...
}

1.11 语法错误

  1. 使用T *int会被编译器认为是表达式 T 乘以 int
type NewType[T *int] []T

使用interface{}或者加上逗号消除歧义

type NewType[T interface{*int}] []T
type NewType2[T interface{*int|*float64}] []T 

// 如果类型约束中只有一个类型,可以添加个逗号消除歧义
type NewType3[T *int,] []T


//✗ 错误。如果类型约束不止一个类型,加逗号是不行的
type NewType4[T *int|*float32,] []T

1.12 泛型嵌套

// 先定义个泛型类型 Slice[T]
type Slice[T int|string|float32|float64] []T

// ✗ 错误。泛型类型Slice[T]的类型约束中不包含uint, uint8
type UintSlice[T uint|uint8] Slice[T]  

// ✓ 正确。基于泛型类型Slice[T]定义的新泛型类型 IntAndStringSlice[T]
type IntAndStringSlice[T int|string] Slice[T]  
// ✓ 正确 基于IntAndStringSlice[T]套娃定义出的新泛型类型
type IntSlice[T int] IntAndStringSlice[T] 

// 在map中套一个泛型类型Slice[T]
type SMap[T int|string] map[string]Slice[T]
// 在map中套Slice[T]的另一种写法
type SMap2[T Slice[int] | Slice[string]] map[string]T

2 泛型接口

2.1 使用接口作为泛型约束条件

type Int interface {
    int | int8 | int16 | int32 | int64
}

type Uint interface {
    uint | uint8 | uint16 | uint32
}

type Float interface {
    float32 | float64
}

type Slice[T Int | Uint | Float] []T  // 使用 '|' 将多个接口类型组合

2.2 ~:指定底层类型

对基础类型重新声明后,例如 type MyInt int,对于泛型类型Slice[int|float32|float64]来说,无法使用Slice[MyInt],只支持基础类型作为类型实参。Go新增~符号,用于适配基础类型,以及基础类型作为底层类型实现的类型,

type Int interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

type Uint interface {
    ~uint | ~uint8 | ~uint16 | ~uint32
}
type Float interface {
    ~float32 | ~float64
}

type Slice[T Int | Uint | Float] []T 

var s Slice[int] // 正确

type MyInt int
var s2 Slice[MyInt]  // MyInt底层类型是int,所以可以用于实例化
  • ~后面的类型不能为接口
  • ~后面必须为基础类型

2.3 泛型类型并集

type Uint interface {  // 类型集 Uint 是 ~uint 和 ~uint8 等类型的并集
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}

2.4 泛型类型交集

type AllInt interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint32
}

type Uint interface {
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}

type A interface { // 接口A代表的类型集是 AllInt 和 Uint 的交集
    AllInt
    Uint
}

type B interface { // 接口B代表的类型集是 AllInt 和 ~int 的交集
    AllInt
    ~int
}

2.5 泛型类型空集

type Bad interface {
    int
    float32 
} // 类型 int 和 float32 没有相交的类型,所以接口 Bad 代表的类型集为空

2.6 空接口和any

空接口interface{}代表所有类型的集合,写入类型约束意味着所有类型都可以拿来做类型实参。

Go1.18开始提供了一个和空接口interface{}等价的关键字any,实际上就是interface{}的别名,两者完全等价。

2.7 comparable(可比较)和可排序(ordered)

在Go语言中,可比较类型是指可以使用==和!=运算符进行比较的类型

  • 可比较类型
    • 数字类型
    • 布尔类型
    • 字符串类型
    • 指针类型:比较指针指向地址是否相同
    • 通道channel:比较通道是否是同一个通道,即是否指向同一个底层数据结构
    • 接口类型:
      • 如果两个接口的动态类型相同且动态值相等,则相等
      • 如果两个接口的动态类型不同,则它们不相等
      • 如果接口的动态值为nil,则与nil比较时相等
    • 数组类型:
      • 如果数组的元素类型是可比较的,则数组可比较,元素逐个比较
    • 结构体类型:
      • 如果结构体的所有字段都是可比较的,则结构体可比较,比较时,按字段逐个比较
  • 不可比较类型
    • 切片 Slice
    • Map
    • func
    • 包含不可比较字段的结构体

在map中,键类型必须是可进行!=和==比较的类型,当我们使用泛型形参来定义map时,可以使用comparable接口,它代表所有可用!=以及==对比的类型 go在 Go 语言中,可进行大小比较的类型是指可以通过运算符(如 <、<=、>、>=)进行比较的类型。这些类型通常具有明确的顺序关系。以下是支持大小比较的类型及其规则:

  1. 基本类型 (1) 整数类型 包括:int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64 等。 比较规则:按数值大小比较。 fmt.Println(3 < 5) // true

    (2) 浮点数类型 包括:float32, float64。 比较规则:按数值大小比较。 fmt.Println(3.14 < 2.71) // false

    (3) 字符串类型 包括:string。 比较规则:按字典序(Unicode 码点)逐个字符比较。 fmt.Println("apple" < "banana") // true

  2. 可比较大小的复合类型 Go 语言本身不支持直接对复合类型(如结构体、数组、切片等)进行大小比较,但可以通过以下方式实现:

(1) 自定义比较函数 例如,对结构体字段进行比较:

type Person struct {
    Name string
    Age  int
}

func compareAge(p1, p2 Person) bool {
    return p1.Age < p2.Age
}

(2) 使用 sort 包

对切片进行排序时,可以定义比较规则:
people := []Person{{"Alice", 25}, {"Bob", 20}}
sort.Slice(people, func(i, j int) bool {
     return people[i].Age < people[j].Age
})
  1. 不可进行大小比较的类型 以下类型不支持直接的大小比较:

(1) 布尔类型 布尔值(true/false)无法比较大小。

// fmt.Println(true < false) // 编译错误

(2) 指针类型 指针(*T)无法比较大小。


(3) 通道类型 通道(chan T)无法比较大小。

ch1 := make(chan int)
ch2 := make(chan int)
// fmt.Println(ch1 < ch2) // 编译错误

(4) 接口类型 接口(interface{})无法直接比较大小。

var i1 interface{} = 10
var i2 interface{} = 20
// fmt.Println(i1 < i2) // 编译错误

(5) 切片、映射、函数

这些类型无法比较大小。


slice1 := []int{1, 2, 3}
slice2 := []int{4, 5, 6}
// fmt.Println(slice1 < slice2) // 编译错误

可进行大小比较的类型被称为 Orderd 。目前Go语言并没有像 comparable 这样直接内置对应的关键词,所以想要的话需要自己来定义相关接口,比如我们可以参考Go官方包golang.org/x/exp/constraints如何定义:

// Ordered 代表所有可比大小排序的类型
type Ordered interface {
    Integer | Float | ~string
}

type Integer interface {
    Signed | Unsigned
}

type Signed interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

type Unsigned interface {
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}

type Float interface {
    ~float32 | ~float64
}

2.8 Go1.18后的接口

在Go1.18之前,Go官方对接口(interface)定义是:接口是一个方法集,即同时定义接口中方法的类型被视为实现这一接口,在Go1.18后,接口理解为实现接口中所有方法的类型集合,将接口定义更改为类型集

Go1.18开始,将接口分为了两种类型:

  • 基本接口(Basic interface)
  • 一般接口(General interface)

基本接口

type MyError interface { // 接口中只有方法,所以是基本接口
    Error() string
}

// 用法和 Go1.18之前保持一致
var err MyError = fmt.Errorf("hello world")

基本接口是指仅包含方法签名的接口,通常用于描述对象的行为而不是具体的实现。

一般接口

// 定义一个一般接口
type Comparable[T any] interface {
    Compare(T) int
}

一般接口是指包含类型约束的接口,通常与Go1.18引入的泛型(generics)相关,描述方法签名,还可以约束类型参数的行为。

特点:

  • 可以包含参数和方法签名
  • 用于定义泛型函数或者泛型类型的行为
  • 类型必须同时满足接口中的方法签名和类型约束

2.9 一般接口的实现

DataProcessor[string]

// 实例化后的接口定义可视为
type DataProcessor[T string] interface {
    int | ~struct{ Data interface{} }

    Process(data string) (newData string)
    Save(data string) error
}

接口意义:

  • 只有实现了Process(string) string 和`` Save(string) error这两个方法,并且以intstruct{ Data interface{} }为底层类型的类型才算实现了这个接口
  • 一般接口(General interface) 不能用于变量定义只能用于类型约束,所以接口 DataProcessor[string] 只是定义了一个用于类型约束的类型集
// XMLProcessor 虽然实现了接口 DataProcessor2[string] 的两个方法,但是因为它的底层类型是 []byte,所以依旧是未实现 DataProcessor2[string]
type XMLProcessor []byte

func (c XMLProcessor) Process(oriData string) (newData string) {

}

func (c XMLProcessor) Save(oriData string) error {

}

// JsonProcessor 实现了接口 DataProcessor2[string] 的两个方法,同时底层类型是 struct{ Data interface{} }。所以实现了接口 DataProcessor2[string]
type JsonProcessor struct {
    Data interface{}
}

func (c JsonProcessor) Process(oriData string) (newData string) {

}

func (c JsonProcessor) Save(oriData string) error {

}
// 错误。DataProcessor2[string]是一般接口不能用于创建变量
var processor DataProcessor2[string]

2.10 接口定义限制

  1. 用 | 连接多个类型的时候,类型之间不能有相交的部分(即必须是不交集),但是相交的类型是接口的情况,不受这一限制。
type MyInt int

// 错误,MyInt的底层类型是int,和 ~int 有相交的部分
type _ interface {
    ~int | MyInt
}
type MyInt int

type _ interface {
    ~int | interface{ MyInt }  // 正确
}

type _ interface {
    interface{ ~int } | MyInt // 也正确
}

type _ interface {
    interface{ ~int } | interface{ MyInt }  // 也正确
}
  1. 类型的并集中不能有类型形参
type MyInf[T ~int | ~string] interface {
    ~float32 | T  // 错误。T是类型形参
}

type MyInf2[T ~int | ~string] interface {
    T  // 错误
}
  1. 接口不能直接或间接并入自己
type Bad interface {
    Bad // 错误,接口不能直接并入自己
}

type Bad2 interface {
    Bad1
}
type Bad1 interface {
    Bad2 // 错误,接口Bad1通过Bad2间接并入了自己
}

type Bad3 interface {
    ~int | ~string | Bad3 // 错误,通过类型的并集并入了自己
}
  1. 接口的并集成员个数大于一的时候不能直接或间接并入 comparable 接口
type OK interface {
    comparable // 正确。只有一个类型的时候可以使用 comparable
}

type Bad1 interface {
    []int | comparable // 错误,类型并集不能直接并入 comparable 接口
}

type CmpInf interface {
    comparable
}
type Bad2 interface {
    chan int | CmpInf  // 错误,类型并集通过 CmpInf 间接并入了comparable
}
type Bad3 interface {
    chan int | interface{comparable}  // 理所当然,这样也是不行的
}
  1. 带方法的接口(无论是基本接口还是一般接口),都不能写入接口的并集中
type _ interface {
    ~int | ~string | error // 错误,error是带方法的接口(一般接口) 不能写入并集中
}

type DataProcessor[T any] interface {
    ~string | ~[]byte

    Process(data T) (newData T)
    Save(data T) error
}

// 错误,实例化之后的 DataProcessor[string] 是带方法的一般接口,不能写入类型并集
type _ interface {
    ~int | ~string | DataProcessor[string] 
}

type Bad[T any] interface {
    ~int | ~string | DataProcessor[T]  // 也不行
}

3.1 泛型的使用

使用Go内置的容器类型

// MapKeys returns a slice of all the keys in m.
// The keys are not returned in any particular order.
func MapKeys[Key comparable, Val any](m map[Key]Val) []Key {
    s := make([]Key, 0, len(m))
    for k := range m {
        s = append(s, k)
    }
    return s
}

实现通用的数据结构

// Tree is a binary tree.
type Tree[T any] struct {
    cmp  func(T, T) int
    root *node[T]
}

// A node in a Tree.
type node[T any] struct {
    left, right  *node[T]
    val          T
}

// find returns a pointer to the node containing val,
// or, if val is not present, a pointer to where it
// would be placed if added.
func (bt *Tree[T]) find(val T) **node[T] {
    pl := &bt.root
    for *pl != nil {
        switch cmp := bt.cmp(val, (*pl).val); {
        case cmp < 0:
            pl = &(*pl).left
        case cmp > 0:
            pl = &(*pl).right
        default:
            return pl
        }
    }
    return pl
}

// Insert inserts val into bt if not already there,
// and reports whether it was inserted.
func (bt *Tree[T]) Insert(val T) bool {
    pl := bt.find(val)
    if *pl != nil {
        return false
    }
    *pl = &node[T]{val: val}
    return true
}

参考链接

https://colobu.com/2021/12/22/no-parameterized-methods/ https://rakyll.org/generics-facilititators/ https://www.cnblogs.com/insipid/p/17772581.html https://medium.com/@dgqypl/go-%E6%B3%9B%E5%9E%8B%E5%9C%A8%E5%AE%9E%E9%99%85%E4%B8%9A%E5%8A%A1%E5%9C%BA%E6%99%AF%E4%B8%AD%E7%9A%84%E5%BA%94%E7%94%A8-7af8ceea7ca

https://github.com/jincheng9/go-tutorial/blob/main/workspace/official-blog/when-to-use-generics.md