我们可以发现 List 这个链表结构其实内部只有两个东西,一个 “前哨” (root)存放了对应的元素和整个链表的长度值。在链表调用初始化函数的时候,我们会发现,它把链表前哨元素的前后指针指向自己,作为一个初始化的流程。这里比较有意思的一点是,这个"前哨"的功能。
你会发现,前哨(root)在初始化的时候并没有给对应的 Value 赋值,开发者的本意其实是利用前哨来存储整个链表的表头和表尾(只用 root 的 prev 和 next 指针)。
前哨的前驱其实是链表的表头,前哨的后继其实是链表的表尾,root 的 prev 存放表尾, next 存放表头。
我们再回头看看之前的节点的两个方法实现:
func (e *Element) Next() *Element {if p := e.next; e.list !=nil&& p !=&e.list.root {return p }returnnil}func (e *Element) Prev() *Element {if p := e.prev; e.list !=nil&& p !=&e.list.root {return p }returnnil}
我们就明白他这里的处理是为什么了,还是要加层 p != &e.list.root 判断,因为List在初始化的时候会把前哨的前后指针指向自己。也就是说,如果当前元素的前后指针指向了 List 的前哨了,也就表示当前节点不存在前驱元素或者是后继元素。
Push 操作
在此之前我们先看两个插入函数:
// insert 代表把 e 插到 at 的后面func (l *List) insert(e, at *Element) *Element { n := at.next // 保存at的后继 at.next = e // 将 at 后继指向 e e.prev = at // 将 e 前驱指向 at e.next = n // 将 e 后继指向原理at的后继 n.prev = e // 将 原来at的后继指向 e e.list = l // 将 e 的 list 指针指向 l l.len++// 链表数量增加return e}// insertValue 其实是对 insert 方法的封装func (l *List) insertValue(v interface{}, at *Element) *Element {return l.insert(&Element{Value: v}, at)}
func (l *List) MoveAfter(e, mark *Element) {if e.list != l || e == mark || mark.list != l {return } l.move(e, mark)}func (l *List) move(e, at *Element) *Element {if e == at {return e } e.prev.next = e.next e.next.prev = e.prev n := at.next at.next = e e.prev = at e.next = n n.prev = ereturn e}
而且很多地方做了 lazyInit(很多地方也叫:延迟初始化),即在必要时才初始化链表(申请内存资源),因为 Go 本身也不知道你存的数据到底是个什么东西,如果存放的值很大,那么一锅脑初始化可能会花费很多时间,而且为了避免声明出来没人使用的尴尬局面,所以通过 lazyInit去分散初始化的操作带来的计算量和存储空间消耗。
比如集中声明非常多的大容量切片, CPU 和内存空间的使用量肯定都会一个激增,并且只有设法让其中的切片及其底层数组被回收,内存使用量才会有所降低。所以把初始化分散再配合 GC 可以更好的利用内存资源同时减少CPU压力。
其实,Go SDK 实现的这个链表不仅仅是双向链表,也可以看成是循环链表 List 的 root 元素恰好把链表的对头和队尾元素连起来,但是 root 本身并不存储元素,它仅仅是个“前哨”。