前言
在开始今天有关泛型的介绍之前,我们先来看一个简单的例子,如果我们要设计两个int
类型变量相加的函数,我们可以这样设计:
func Sum (a, b int) int { return a + b } • 1 • 2 • 3
如果变量要求是float
类型或者是其他类型,我们要面对一个问题:我们真的要为每个类型去编写对应的函数吗当然这个是非常影响开发效率的,而我们要如何解决这个问题呢?这就是我们几天要提到的泛型了。
什么是泛型
泛型,顾名思义,它是为了执行逻辑与类型无关的问题,这类问题不关心给出的类型是什么,只需要完成对应的操作就足够,我们可以尝试把上面的问题改成泛型的写法:
func Sum [T int|float64](a, b T) T{ return a + b } • 1 • 2 • 3
类型形参:T就是一个类型形参,形参具体是什么类型取决于传进来什么类型
类型约束:int | float64
构成了一个类型约束,这个类型约束内规定了哪些类型是允许的,约束了类型形参的类型范围
类型实参:Sum[int](1,2)
,手动指定了int
类型,int
就是类型实参。
第一种用法,显式的指明使用哪种类型,如下
Sum[int](2012, 2022) • 1
第二种用法,不指定类型,让编译器自行推断,如下
Sum(3.1415926, 1.114514) • 1
通过上面的介绍,相信大家对泛型有所了解了,在日常使用泛型时,我们要注意将泛型引入项目后,开发上确实会比较方便,随之而来的是项目复杂度的增加,毫无节制的使用泛型会使得代码难以维护,所以应该在正确的地方使用泛型,而不是为了泛型而泛型。
泛型结构
泛型切片
首先我们定义一个泛型切片,它的类型约束时int|float32|float64
:
type GenericsSlice[T int|float32|float64] []T
我们如何去使用这种泛型切片呢?让我们来看一下下面这个案例:
package main import "fmt" func main() { type GenericsSlience[T int | float64] []T //创建整形切片 var ints GenericsSlience[int] ints = append(ints, 1) //创建浮点型切片 var floats GenericsSlience[float64] floats = append(floats, 1.1) //打印切片 fmt.Println(ints) fmt.Println(floats) }
输出为:
泛型哈希表
我们定义泛型哈希表的时候,要保证键的类型必须是可以比较的,所以我们会使用comparable
接口,下面我们可以尝试定义一个哈希表:
type GenericsMap[K comparable,V int|string|byte] map[K]V
使用时:
gmap1 := GenericMap[int, string]{1: "hello world"} gmap2 := make(GenericMap[string, byte], 0)
泛型结构体
这是一个泛型结构体,类型约束为T int | string
解释type GenericStruct[T int | string] struct { Name string Id T }
使用:
解释GenericStruct[int]{ Name: "jack", Id: 1024, } GenericStruct[string]{ Name: "Mike", Id: "1024",
注意:在结构体中如果我们要使用切片,一般推荐下面这种写法:
type Company[T int | string, S int | string] struct { Name string Id T Stuff []S }
泛型接口
我们来看一个泛型接口的简单应用:
package main import "fmt" type Sayable[T int | float64 | string] interface { Say() T } type Person[T int | float64 | string] struct { msg T } func (p Person[T]) Say() T { return p.msg } func main() { var s Sayable[string] s = Person[string]{"hello world"} fmt.Println(s.Say()) }
泛型结构的使用注意点
- 泛型不能作为一个类型的基本类型
- 泛型类型无法使用类型断言
- 匿名结构体/函数不支持泛型
- 不支持泛型方法,这里要说明一下,主要是不支持泛型形参,比如下面这样:
package main import "fmt" type Sayable[T int | float64 | string] interface { Say() T } type Person[T int | float64 | string] struct { msg T } func (p Person[T]) Say() T { return p.msg } func main() { var s Sayable[string] s = Person[string]{"hello world"} fmt.Println(s.Say()) }
这样是无法通过编译的。
泛型使用示例
模拟队列
package main type Queue[T any] []T func (q *Queue[T]) Push(e T) { *q = append(*q, e) } func (q *Queue[T]) Pop() (_ T) { if q.Size() > 0 { res := q.Peek() *q = (*q)[1:] return res } return } func (q *Queue[T]) Peek() (_ T) { if q.Size() > 0 { res := (*q)[0] return res } return } func (q *Queue[T]) Size() int { return len(*q) }
在Pop
和Peek
方法中,可以看到返回值是_ T
,这是具名返回值的使用方式,但是又采用了下划线_
表示这是匿名的,这并非多此一举,而是为了表示泛型零值。由于采用了泛型,当队列为空时,需要返回零值,但由于类型未知,不可能返回具体的类型,借由上面的那种方式就可以返回泛型零值。也可以声明泛型变量的方式来解决零值问题,对于一个泛型变量,其默认的值就是该类型的零值
堆
上面队列的例子,由于对元素没有任何的要求,所以类型约束为any
。但堆就不一样了,堆是一种特殊的数据结构,它可以在O(1)的时间内判断最大或最小值,所以它对元素有一个要求,那就是必须是可以排序的类型,但是go语言内置可以比较的类型就只有字符串和数字,同时泛型约束不允许带方法的接口,所以在对初始化的时候就需要我们传入一个自定义的比较器
下面我们来尝试来实现一个简单的最小二根堆:
package main type Comparator[T any] func(a, b T) int type BinaryHeap[T any] struct { data []T comparator Comparator[T] } func (Heap *BinaryHeap[T]) Peek() (_ T) { if Heap.Size() > 0 { return Heap.data[0] } return } func (Heap *BinaryHeap[T]) Pop() (_ T) { if Heap.Size() > 0 { res := Heap.Peek() Heap.data = Heap.data[1:] return res } return } func (Heap *BinaryHeap[T]) Push(value T) { Heap.data = append(Heap.data, value) Heap.up(Heap.Size() - 1) } func (Heap *BinaryHeap[T]) Size() int { return len(Heap.data) } func (Heap *BinaryHeap[T]) up(i int) { if Heap.Size() == 0 || i < 0 || i >= Heap.Size() { return } for parentindex = i>>1 - 1; parentindex >= 0; parentindex = i>>1 - 1 { if Heap.comparator(Heap.data[i], Heap.data[parentindex]) < 0 { Heap.data[i], Heap.data[parentindex] = Heap.data[parentindex], Heap.data[i] i = parentindex } else { break } } } func (Heap *BinaryHeap[T]) down() { if heap.Size() == 0 || i < 0 || i >= heap.Size() { return } size := heap.Size() for lsonIndex := i<<1 + 1; lsonIndex < size; lsonIndex = i<<1 + 1 { rsonIndex := lsonIndex + 1 if rsonIndex < size && heap.compare(heap.s[rsonIndex], heap.s[lsonIndex]) < 0 { lsonIndex = rsonIndex } // less than or equal to if heap.compare(heap.s[i], heap.s[lsonIndex]) <= 0 { break } heap.s[i], heap.s[lsonIndex] = heap.s[lsonIndex], heap.s[i] i = lsonIndex } }
使用起来如下
type Person struct { Age int Name string } func main() { heap := NewHeap[Person](10, func(a, b Person) int { return cmp.Compare(a.Age, b.Age) }) heap.Push(Person{Age: 10, Name: "John"}) heap.Push(Person{Age: 18, Name: "mike"}) heap.Push(Person{Age: 9, Name: "lili"}) heap.Push(Person{Age: 32, Name: "miki"}) fmt.Println(heap.Peek()) fmt.Println(heap.Pop()) fmt.Println(heap.Peek()) }
输出
{9 lili} {9 lili} {10 John}
有泛型的加持,原本不可排序的类型传入比较器后也可以使用堆了,这样做肯定比以前使用interface{}
来进行类型转换和断言要优雅和方便很多。
总结
go的一大特点就是编译速度非常快,编译快是因为编译期做的优化少,泛型的加入会导致编译器的工作量增加,工作更加复杂,这必然会导致编译速度变慢,事实上当初go1.18刚推出泛型的时候确实导致编译更慢了,go团队既想加入泛型又不想太拖累编译速度,开发者用的顺手,编译器就难受,反过来编译器轻松了(最轻松的当然是直接不要泛型),开发者就难受了,现如今的泛型就是这两者之间妥协后的产物。