为什么要使用泛型?
Go 泛型的核心价值:
- 减少重复代码:同一逻辑支持多种类型,避免为不同类型写相同函数。
- 类型安全:替换
interface{}
,编译时检查类型,避免运行时错误。 - 性能优化:比反射或
interface{}
更高效,无运行时开销。 - 标准库增强:支持通用工具(如
slices
、maps
包)
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 语法错误
- 使用
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) 整数类型 包括: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
可比较大小的复合类型 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) 布尔类型 布尔值(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
这两个方法,并且以int
或struct{ 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 接口定义限制
- 用 | 连接多个类型的时候,类型之间不能有相交的部分(即必须是不交集),但是相交的类型是接口的情况,不受这一限制。
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 } // 也正确
}
- 类型的并集中不能有类型形参
type MyInf[T ~int | ~string] interface {
~float32 | T // 错误。T是类型形参
}
type MyInf2[T ~int | ~string] interface {
T // 错误
}
- 接口不能直接或间接并入自己
type Bad interface {
Bad // 错误,接口不能直接并入自己
}
type Bad2 interface {
Bad1
}
type Bad1 interface {
Bad2 // 错误,接口Bad1通过Bad2间接并入了自己
}
type Bad3 interface {
~int | ~string | Bad3 // 错误,通过类型的并集并入了自己
}
- 接口的并集成员个数大于一的时候不能直接或间接并入 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} // 理所当然,这样也是不行的
}
- 带方法的接口(无论是基本接口还是一般接口),都不能写入接口的并集中
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