go的container包解读
1.introduction
在日常写缓存的时候经常遇到可以使用双向链表的情况,而这恰恰中了go的list包下怀。也许list包已经用的熟练了,但是这种强劲数据结构不免引人想去看看官方是怎么实现的。与此同时,我发现了这个包身边还伴随两个强劲的伙伴——ring和heap(环形链表和堆)。
他们统一都在container包中,接下来开始阅读。
2.list
众所周知list是一个双向链表。链表有节点,我们平时叫他“node”,但是在这里是一个叫element的结构
type Element struct { |
其中list很特殊,它代表一个list的容器。进一步说,是把一条双向链表给包装起来。这里的list指向“belongs to”
type List struct { |
这样一看就很明显了,一个链表有root(头一个元素),len(长度)。初步可以理解成这样
神奇的一幕来了。我们之前认为New函数为非结构体函数,然而这里给出了一种诡异的思路。
// Init initializes or clears list l. |
也就是说,先用一个匿名的List调用Init方法,然后再进行返回。虽然如此,但是最终暴露给外界的也是一个非结构体函数。
可能只是google程序员脑洞做的一种尝试吧。总之达到的结果似乎没什么值得惊叹的地方。
剩下的方法就是对于头、尾中间的CRUD。
3.ring
环,没有特别可说的地方。注意它的next指的是“前进方向”。他的节点叫ring。
type Ring struct { |
我们来讲讲一些特别的方法。
// Move moves n % r.Len() elements backward (n < 0) or forward (n >= 0) |
Move,做旋转,假设前进方向为顺时针,n为正就顺时针,n为负就逆时针。
func (r *Ring) Link(s *Ring) *Ring { |
对于Link(环的连接),我找到了很好的解析图。
对于不同的两个环:
而对于同一个环上的两个节点则是去做拆分,则是做一个截取。
这个设计非常奇葩,首先就是在使用的时候,我们需要对返回的值是谁有清晰的认识。其次,我们需要认识清楚它是同一个环上的操作还是不同环上的操作。这要求对源码实现有深刻的理解。
总之,记住返回的是n,n之前一定是ρ。
func (r *Ring) Unlink(n int) *Ring { |
unlink删除next开始的n个元素。这里的实现很有意思,用了link。也就是说同一个环,我保留r到r向前推进n+1之间的环。
func (r *Ring) Do(f func(any)) { |
Do,对于环上每个r,执行f(r)。
有什么使用场景呢?其实它最关键的特点是代表了一个双向链表首尾相连。所以我们要找收尾无延迟跳转的场景。
比如,循环队列。游戏里那种反复点击某个按钮进行装备切换这种操作。
固定大小循环缓冲区,比如我要保留多少大小的日志,那么达到容量老的就自动就被覆盖。
轮询调度,比如我们最近在设计网关的时候,就可以对某个服务的多个地址采取轮询调度的策略。
注:轮询调度(Round-Robin)是指按顺序逐个访问,而轮询(Polling)是指时间上周期性访问,不要混淆。
3.heap
heap留到最后,不仅因为它的场景关键,而且实现也复杂。
这个堆结构在container包特殊的地方在于,它提供了接口让开发者自己实现更多的扩展,而非直接提供了一个结构体。
先了解下sort包的接口。
type Interface interface { |
基本的排序,我们我们有比较和交换的环节。同时,遍历需要知道它的长度属性。
而heap的接口,继承sort的同时增加了push和pop
type Interface interface { |
目前我们可以认为,他是一个可排序的栈。对于可排序的栈(如优先队列)来说,我们不难想到这里的heap其实是一个数组,或者说slice的形式存在的。这与我们认知中的完全二叉树(节点实现)可能有一些出路。
在已有知识体系中,我们知道大小堆,而堆的排序叫做“下沉”。down返回bool代表该操作是否发生交换逻辑。
func down(h Interface, i0, n int) bool { |
这里可能需要解释一下。i0代表开始下沉的根。j1 := 2*i + 1中,j1代表i的左子节点,这是二叉树的特性。如果到这里你已经懵逼了,那我不得不画个图让你秒懂了。
没错,你已经懂了。那么显然,2*i+2就是右子节点。
上图是i,j1,j2的关系。假设less是判断左边参数更小,那么这就是一个小根堆,因为它找到了更小的值给了赋给了i。
接下来看初始化
func Init(h Interface) { |
也是一张图就可以看懂它是怎么构建出来一个小根堆的
然后看up是怎么做上浮的
func up(h Interface, j int) { |
无需多言。
heap由于是接口形式,因此它不可持有方法。我们采用非结构体函数形式存在。有了以上基础操作实现,heap包级函数实现将比较轻松。
func Push(h Interface, x any) { |
push(back)之后做上浮。
func Pop(h Interface) any { |
pop(front)之后做下沉。
func Remove(h Interface, i int) any { |
移除操作,也是下沉。
func Fix(h Interface, i int) { |
Fix,更改值之后重新建立秩序。它的开销会比remove之后再push小,因为是原地更改。
至此,实现一个优先队列进行练手。优先队列非常有用,比如经常用到的延时队列(超时、定时自动取消)就可以用它实现。
注:引自sdk的 container/heap/example_pq_test.go
// An Item is something we manage in a priority queue. |
关键点:
1.index代表外界的下标,因此swap时候需要主动更新。
2.优先队列是大根堆,所以用大于号
3.update为修改值,也就是说支持动态调整,这是对于传统优先队列改进的地方。换句话说,它不是单一个可排序的栈,甚至可以修改值进行动态调整,这在生产环境中非常优越,比如我可以修改定时任务的值。
4.end
累了,毁灭吧。