Golang 泛型编程
这里涉及到要学习泛型是由于工程里面遇到一个问题,karmem 下在序列化和反序列化的时候所编写的代码在很大程度上是相似的
于是解决方案就是为不同的结构体都写对应的序列化和反序列化函数来封装
1
2
3
4
5
6 func DeserializeGeneralParams(byteParamsData []byte) (*common.GeneralParams, error) {
generalParams := new(common.GeneralParams)
generalParams.ReadAsRoot(karmem.NewReader(byteParamsData))
return generalParams, nil
}但是这样又存在一个问题,每次新增结构体都需要 copy 新的函数为它进行反序列化和序列化,那么就特别麻烦,所以考虑能不能通过泛型实现类似下面的操作
1
2
3
4
5
6 func DeserializeKarmemStruct[T KarmemStruct](byteParamsData []byte) (*T, error) {
generalParams := new(T)
T.ReadAsRoot(karmem.NewReader(byteParamsData))
return generalParams, nil
}也就是将一系列 Karmem 生成的类的反序列化放到一起,现在看完了泛型的一系列东西后貌似无法解决这个问题(?
形参和实参
在一个简单的实现加法的函数中,函数上写明的 a int, b int
是形参(Parameter)列表,只有在调用时传入的值才是实参(Argument)
1 | func Add(a int, b int) int { |
将形参和实参的概念进行推广,给类型引入类似形参和实参的概念后,就可以有类型形参(Type Paramether) 和类型实参(Type Argument)
1 | func Add(a T, b T) T { |
T
是这里的类型形参,在定义函数时类型补确定,只有在函数被调用时再传入具体的类型,这里被传入的具体类型就是类型实参
传入类型实参的方法
1 | Add[T=int](100, 200) |
泛型约束
问题提出
考虑一个例子
1 | type IntSlice []int |
b
的赋值不能实现,这是由于 IntSlice
的底层类型是 []int
,浮点类的 Slice 不能进行赋值
如果需要不同类型的切片,可以考虑为每种类型都定义新的类型
1 | type StringSlice []string |
解决方法
这样为每个不同的成员类型定义新类型的方法极为繁琐,所以就要使用到泛型
1 | type Slice[T int|float32|float64] []T |
T
是类型形参(Type Parameter),定义 Slice 时其代表的类型不确定,它是一个占位符int|float32|float64
是类型约束(Type Constraint),用于说明类型形参只能接受哪些类型的实参(有点类似于对形参的类型定义)T
和int|float32|float64
构成了类型形参列表(Type Parameter List)
泛型函数
使用类型形参来替代原有的类型,可以定义一个泛型函数
1 | func Add[T int|float32|float64] (a T, b T) T { |
和之前实现不同的是这里添加了类型约束,保证函数的类型只能使用这三种类型,使用泛型函数需要传入类型实参
1 | Add[int](1, 2) |
也可以直接传入参数让编译器自行推导类型实参(让编译器推导类型后传入参数)
1 | Add(1, 2) |
需要注意的是下面两种情况不能使用泛型函数:
在匿名函数中使用未定义的类型形参(但是可以使用已定义的形参)
不支持泛型方法,即 receiver 下不能支持泛型
1
2
3
4type A struct {}
func (receiver A) Add[T int|float32|float64](a T, b T) T {
return a + b
}但是可以通过 receiver 来使用类型形参
1
2
3
4
5
6
7
8
9
10
11
12type A[T int | float32 | float64] struct {
}
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 | type Slice[T int|int8|int16|int32|...|uint32|uint64] []T; |
为了实现对这样的代码进行维护,可以对类型进行分类后把类型约束写入到接口类型中,即可以在类型约束中进行嵌套
1 | type Int interface { |
如果要使得最终的类型定义更为简单,可以在 interface
中进行嵌套
1 | type SliceElement interface { |
通过 interface
进行嵌套并在类型约束中进行组合的方式,就可以使得后续对代码的维护更为方便。但是这样的方式存在的缺陷是,如果类型约束中包含了一种类型 type1
,另外一种类型通过 type1
定义了 type type2 type1
,这样的情况下就不能满足类型约束条件
1 | var s1 Slice[int] |
这样的代码在第四行是错误的,因为虽然其底层类型是 int
,但是它自身并不是 int
类型,不符合类型约束
于是可以使用 ~
来表示类型约束的底层类型,例如 type Slice [T ~int|~float32]
,这样只要底层类型满足约束条件即可,使用该符号的限制是
- 符号后类型不能是接口
- 符号后类型必须为底层类型
类型集
在 Go 1.18 之前, Go 官方对接口的定义是:
An interface type specifies a method set called its interface
对于 ReaderWriter
接口,它定义了一个接口,这个集合中包含了 Read
和 Write
两种方法,所有同时定义了这两种方法的类型被称为实现了该接口
1 | type ReaderdWriter interface { |
从另外一个角度来看,ReaderWriter
可以是一个类型的集合,所有实现了其包含的两个方法的类型都在接口代表的类型集合中,这样就是一个类型集(Type Set)
在可以使用接口来简化类型约束后,接口就有了定义类型集的作用,在原来的功能上,它只是定义一系列的方法,所以是方法集
1 | type SliceElement interface { |
在这里,类型接口 SliceElement
是一个类型集合,所有满足约束 Int|Uint
的类型都在这一类型集中
type Slice[T SliceElement] []T
中,类型约束指定了形参可接受的类型集合,只有属于这个集合中的类型才能替换形参用于实例化
原有的实现某个接口的定义是:实现了一个接口的所有方法那么就隐式地实现了这个接口
而现在,如果类型 T 满足下面的条件,那么就说它实现了接口 I:
- T 不是接口时:类型 T 是接口 I 代表的类型集中的一个成员
- T 是接口时:T 接口代表的类型集是 I 代表的类型集合的子集
基本接口和一般接口
基本接口:在 Go 1.18 前的方法集,即接口定义中只有方法的接口被称为基本接口
一般接口:而如果一个接口中不只有方法,还有类型,那么这样的接口就被称为一般接口
一般接口类型不能用于定义变量,只能用于进行泛型的约束
到这里该写的也写的差不多了,剩下一些东西可以在 https://segmentfault.com/a/1190000041634906 里翻一下,里面写的比较详细