Go-知识泛型
- 1. 认识泛型
- 1.1 不使用泛型
- 1.2 使用泛型
- 2. 泛型的特点
- 2.1 函数泛化
- 2.2 类型泛化
- 3. 类型约束
- 3.1 类型集合
- 3.2 interface 类型集合
- 3.2.1 内置interface类型集合
- 3.2.2 自定义interface类型集合
- 3.2.2.1 任意类型元素
- 3.2.2.2 近似类型元素
- 3.2.2.3 联合类型元素
- 3.2.3 interface类型集合运算
- 3.2.4 基于操作的类型集合
- 4. 小例子
- 4.1 map.Keys 获取map的全部key
- 4.2 Set
- 4.3 排序 Sort
- 5. 总结
泛型是程序设计语言的一种风格或范式,允许程序员在编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。
Java,C++等多种编程语言都支持泛型,Go语言从1.18版本起也开始支持泛型。
1. 认识泛型
1.1 不使用泛型
实现两个函数对map的value值进行累加,一个是int64类型的值,一个是float64类型的值
func SumInt64(m map[string]int64) int64 {
var sum int64
for _, v := range m {
sum += v
}
return sum
}
func SumFloat64(m map[string]float64) float64 {
var sum float64
for _, v := range m {
sum += v
}
return sum
}
接着使用两个函数
func TestSum(t *testing.T) {
ints := map[string]int64{
"one": 234,
"two": 6755,
"three": 78675,
}
floats := map[string]float64{
"one": 123.456,
"two": 7865.9658,
"three": 87906.865,
}
t.Logf("sum ints : %v , floats : %v", SumInt64(ints), SumFloat64(floats))
}
执行如下:
实现同样的功能,就因为处理的数据类型不一样,就需要为每种类型编写类似的重复代码。
1.2 使用泛型
泛型函数,就是吧函数的参数和返回值“泛化”,使逻辑通用。 通用并不是对所有类型都使用,所以在声明泛型函数时,需要声明适用的“参数类型”(类型约束).
func SumValue[K comparable, V int64 | float64](m map[K]V) V {
var sum V
for _, v := range m {
sum += v
}
return sum
}
SumValue泛型函数通过[K comparable, V int64|fload64]
声明了两个类型参数K,V,供函数参数和返回值使用。
类型参数K的类型必须为comparable类型,因为被用作map的key值,在Go语言中map的key值必须是可比较的类型。
类型参数V的类型可以是int64或float64,在声明时使用|
组合支持的类型。
普通参数m 表示一个泛化的map,相应的返回值也是一个泛化的类型。
使用:
func TestSumValue(t *testing.T) {
ints := map[string]int64{
"one": 234,
"two": 6755,
"three": 78675,
}
floats := map[string]float64{
"one": 123.456,
"two": 7865.9658,
"three": 87906.865,
}
t.Logf("sum ints : %v , floats : %v", SumValue(ints), SumValue[string, float64](floats))
}
执行结果
在调用泛型函数的地方,编译器会将泛型函数实例化,即使用真实的类型来替换类型参数,在调用时有两种方式:
- 隐式调用: 调用泛型函数时使用缺省类型参数,让编译器根据实际的参数进行推导。(SumValue(ints))
- 显示调用: 调用泛型函数时显式的指明类型参数。(SumValuestring, float64)
需要注意的是,编译器之所以能够推导出参数类型是因为函数存在入参,编译器通过传入的变量和函数的参数声明可以推导出参数类型,但对于没有函数参数的泛型函数来说,编译器
无法进行推导,也就无法实例化泛型函数,此时必须显式地调用并指定类型参数。
比如:
就无法推导出来返回值的类型了
而且显式调用,必须全部指定,不能指定部分类型
2. 泛型的特点
泛型的英文表述为generic,即一般化,泛化,具体来讲就是函数和类型的泛化。在泛型被引入之前一个函数所能接收的参数类型及
所能处理的数据类型是确定的,但泛型函数却能接受和处理多种类型,对于类型也是同样的道理。
泛型主要包含三方面的内容:
- 函数的泛化
- 类型的泛化
- 接口的扩展
2.1 函数泛化
为了支持泛型,Go语言函数扩展为可以接受一个使用方括弧表示的类型参数;
func SumValue1[K comparable, V int64 | float64, T int64 | float64](m map[K]V) T
同普通的函数参数类似,类型参数中的每个参数也有一个类型,比如参数K,V的类型分别为 comparable(伴随泛型而引入的内置interface类型,表示可比类型)
和int64|float64(表示int64或float64)。
函数中的类型参数是可选的,没有类型参数的函数是传统的函数,带有类型参数的函数则是泛型函数。
即便Go 1.18 引入了泛型并且扩展了函数,但仍然保持兼容(在这里小小的谴责一下 python 和 scala )。
类型参数中的类型正式的名称是类型约束,用来约束类型的范围。 上面额函数可以接受多种类型的map为参数,考虑到所有的map的key的类型都是comparable类型,
那么只要一个map的值类型是int64或float64就能调用泛型函数。
比如: map[int]int64,map[int64]int64,map[float64]float64,...
2.2 类型泛化
泛型同样扩展了类型的表示方法,允许在创建自定义类型时也能接受一个使用方括弧表示的类型参数。
type arr[T int|int64] []T
声明arr类型,可以容纳int或者int64的切片。
这种声明中带有类型参数的类型被称为泛型类型。
泛型类型必须通过类型参数实例化后才可以使用。
var arrInt arr[int]
var arrInt64 arr[int64]
但是当实例化允许范围之外的类型时,会编译异常
在实例化一个泛型类型时,必须指定类型参数(编译器无法自动推导)
泛型类型同普通类型一样,同样允许定义方法,但是其类型必须带上类型参数:
func (a *arr[T]) add(x T) {
*a = append(*a, x)
}
func TestArrAdd(t *testing.T) {
var a arr[int]
a.add(3)
a.add(4)
t.Logf("res : %v", a)
}
执行结果
未泛型类型定义方法时,必须指定类型参数,但参数名可以与泛型类型声明不同。
func (a *arr[X]) add1(x X) {
*a = append(*a, x)
}
定义时使用T,但是在使用的时候,可以与声明时的名字不同。
如果方法体中并未使用类型参数,甚至可以使用_
省略
func (a *arr[_]) add2(x _) {
*a = append(*a, x)
}
但是不管是换个名字还是使用_
并没有任何好处,反而降低了可读性。
3. 类型约束
无论函数和类型如何泛化,都需要类型参数来限定其泛化的范围,类型参数使用类型的集合表示范围。
3.1 类型集合
func SumValue1[K comparable, V int64 | float64, T int64 | float64](m map[K]V) T
该函数的类型参数中K的类型限定为comparable,V 的类型限定为int64或float64。
comparable是interface类型,int64|float64是组合类型,都代表一个类型集合,用于约束泛化的范围。
使用|
来组合多个类型,从而形成一个类型集合。
3.2 interface 类型集合
在泛型特性被引入之前,interface仅表示一个方法集合,实现了该方法结合的所有类型都可认为实现了这个interface。
在泛型的设计中,对interface进行了扩展,interface将不在仅仅表示方法集合,它还可用于表示类型集合,同理,集合内所有类型都可认为实现了这个interface。
3.2.1 内置interface类型集合
comparable就是跟随泛型被引入的内置interface类型
除了comparable还有any。
comparable表示可比较类型的集合,仅能用于类型参数中。
any不仅在类型参数中表示任意类型的集合,还可以在非泛型场景中作为interface{}的别名使用。
3.2.2 自定义interface类型集合
除了内置的comparable和any两种类型可作为类型约束使用,用户还可以使用interface来定义类型集合。
在泛型之前,interface类型中仅允许包含方法或内嵌interface两种元素,引入泛型后,interface类型将允许使用另外三种元素以表示一个类型集合:
- 任意类型元素(如 int)
- 近似类型元素(使用表示法,如int)
- 联合类型元素(使用|表示法,如int|int64)
需要注意的是,如果interface类型中使用了这三种元素的任意一种,那么这个interface只能用于泛型的类型参数
3.2.2.1 任意类型元素
任意类型(包含interface类型)都可以出现在一个新的interface类型中,用于表示一个仅用于类型参数的集合
type Mint interface {
int
}
func addMint[M Mint](m1, m2 M) M {
return m1 + m2
}
func TestMint(t *testing.T) {
t.Log(addMint(3, 4))
}
此时该interface表示的数据集仅包含一种类型,且仅能用于泛型场景中的类型参数中。
interface类型和自定义类型都可以出现在interface中从而表示一个类型集合。
可以定义新的泛型interface,不能使用泛型interface定义interface 方法集合
interface泛型用于interface
但是不能将interface泛型用于interface方法集合
但是如果显式的声明了泛型,那么就可以使用
并且该interface的方法集合也能像之前一样实现
因为在定义interface泛型的时候,限定是int,所以只有int类型才算是实现了方法
这样来看,使用interface泛型类型,可以限定什么样的方法算是实现。
如果将float64加入到interface泛型中,那么float64的方法也算是实现
3.2.2.2 近似类型元素
在使用 interface声明类型集合时,可以使用~<type>
来制定一组类型,只要其底层类型为同一类型即包含在这个集合中。
因为在Go中,可以通过type取别名,而泛型又时通用这个含义。
比如创建一个string的泛型函数,但是因为使用了type对string取了别名,结果别名类型就无法使用泛型函数。
近似类型元素就是可以让type取了别名的类型也能使用
不使用近似类型
使用近似类型
只要底层类型相同,就能使用泛型函数
需要注意的是,
~
之后的类型必须是某个底层类型,换句话说,一个类型的底层类型不是自身就不能使用~
。
另外,interface 自身也不能用于定义近似类型集合,因为interface的底层类型并不确定。
3.2.2.3 联合类型元素
前面使用~
定义的元素集合仅能包括一组底层类型一致的类型,又是可能需要联合多种类型,甚至联合多种底层类型一致的类型,此时可以用
|
定义一个更宽泛的类型集合
type MInteger interface {
int | int8 | int16 | int32 | int64
}
但是上述定义仅能支持底层类型,不支持别名
更进一步,可以把所有底层类型也包含进来
type MAnyInteger interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
这样即使是别名类型,也能支持。
3.2.3 interface类型集合运算
前面使用interface声明类型集合时,均使用一行代码制定一个集合(一个子集),事实上interface支持按行制定多个自己和,这些自己和取交集形成最终的集合
type NewString interface {
~string
string
}
NewString的类型集合由两个子集组成,一个是所有底层类型为string的集合,另一个是string单一类型,两个子集取交集,最终的类型集合将只包含string单一类型
3.2.4 基于操作的类型集合
假设顶一个泛型函数来比较元素大小
type Ordered interface {
~int|~int8|~int16|~int32|~int64|
~uint|~uint8|~uint16|~uint32|~uint64|
~float32|~float64|
~string
}
func Equals[T Ordered](a, b T) bool {
if a == b {
return true
}
return false
}
定义的Ordered泛型类型是全部可以使用==
比较的底层类型,并且包含别名类型,泛型函数限定了Ordered泛型类型。
copmarable和Ordered类似,范围更大。
4. 小例子
4.1 map.Keys 获取map的全部key
将map中的所有key取出来,然后存入切片中返回
func Keys[K comparable, V any](m map[K]V) []K {
res := make([]K, 0, len(m))
for k, _ := range m {
res = append(res, k)
}
return res
}
类型参数K被用于声明了一个泛型的切片,然后把遍历到的key添加到切片中并返回。
任意的map都能使用Keys泛型函数
func TestKeys(t *testing.T) {
t.Log(Keys(map[string]struct{}{
"one": {},
"tow": {},
"three": {},
}))
t.Log(Keys(map[int]int{
1: 1,
2: 2,
3: 3,
}))
}
4.2 Set
Set可以存储一组不重复的数据,广泛用于需要去重的场景。很多编程语言比如Java,C++都提供了相应的实现,但是在Go语言中并没有Set类型。
有了泛型可以自己实现了
// 定义 Set
type Set[T comparable] map[T]struct{}
// 创建 Set
func MakeSet[T comparable]() Set[T] {
return make(Set[T])
}
// 添加元素
func (s Set[T]) Add(k T) {
s[k] = struct{}{}
}
// 删除元素
func (s Set[T]) Delete(k T) {
delete(s, k)
}
// 判断是否包含
func (s Set[T]) Contains(k T) bool {
_, ok := s[k]
return ok
}
// 返回长度
func (s Set[T]) Len() int {
return len(s)
}
// 遍历
func (s Set[T]) Iterate(f func(T)) {
for k := range s {
f(k)
}
}
func TestSet(t *testing.T) {
set := MakeSet[int]()
set.Add(1)
set.Add(2)
set.Add(1)
set.Add(3)
t.Log(set.Len()) // 预期 3
t.Log(set.Contains(2)) // 预期 true
set.Delete(1)
t.Log(set.Len()) // 预期 2
t.Log(set.Contains(1)) // 预期 false
sum := 0
set.Iterate(func(i int) {
sum += i
})
t.Log(sum) // 预期 5
}
使用泛型实现的Set可适用于任意的可比较类型,行为与其他语言实现的Set基本类似,但是只能函数调用,不能使用下标操作访问元素。
上面实现的Set底层使用一个map实现,并不是线程安全的,还可以进一步使用自定义扩展
type SyncSet[C comparable] struct {
l sync.RWMutex
m map[C]struct{}
}
在读操作的时候,加读锁,在写操作的时候加写锁。
4.3 排序 Sort
要对切片中的元素进行排序,在标准库提供sort.Slice之前,每种类型的切片都需要实现sort.Interface接口中的三个方法
然后使用sort.Sort方法进行排序,即便后来标准库中引入了sort.Slice,但是使用时仍然需要提供一个排序函数。
使用泛型实现一个针对切片的通用排序函数
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64 |
~string
}
type orderSlice[O Ordered] []O
func (o orderSlice[O]) Len() int {
return len(o)
}
func (o orderSlice[O]) Less(i, j int) bool {
return o[i] < o[j]
}
func (o orderSlice[O]) Swap(i, j int) {
o[i], o[j] = o[j], o[i]
}
func OrderSlice[O Ordered](s []O) {
// s 被转换为 []Ordered 类型,也就是 orderSlice,然后排序
// 因为 orderSlice已经实现了排序的接口,不需要额外实现了
sort.Sort(orderSlice[O](s))
}
func TestOrder(t *testing.T) {
is := []int{3, 4, 5, 1, 2}
OrderSlice(is)
t.Log(is)
ss := []string{"he", "ww", "ss"}
OrderSlice(ss)
t.Log(ss)
}
使用泛型实现排序幻术也有一定的局限性,因为不容易处理复杂的符合类型,比如自定义的struct类型。
5. 总结
反形式衡量编程语言技术完备度的一个重要参考指标,但是也是一个比较争议的技术。
泛型的缺失导致开发者不得不编写重复的代码,或者编写相对通用但缺少类型安全的代码,甚至有些项目使用代码自动生成技术来摆脱编写
重复代码的烦恼,从这方面来看,Go确实需要泛型。
但是引入泛型也是有一定成本的,比如泛型的三个困局:
- 没有泛型(C语言)会降低程序员的生产力,但不会增加语言的复杂度
- 泛型会增加编译器的负担(C++),可能会编译出很多冗余的代码,进而拖慢编译时间
- 泛型会降低运行时的性能(Java),避免编译大量冗余代码的后果是增加运行时的开销
Go语言早在1.17版本时就推出了试用版本,但在1.18中还是用了极大的篇幅说明泛型的种种风险。
https://golang.google.cn/doc/go1.18#generics