深入了解Golang官方container/list原理
開篇
我們繼續(xù)上一篇 解析 Golang 官方 container/heap 用法 學(xué)習(xí) Golang 的標準庫 container 中,包含的常見的數(shù)據(jù)結(jié)構(gòu)的實現(xiàn),今天的主角是 container/list。
container/list 封裝了雙向鏈表的實現(xiàn),其實在面試中我們經(jīng)常被問到如何實現(xiàn)一個雙向鏈表,雖然并不難,但總會有邊邊角角的處理需要小心。今天,我們就來結(jié)合源碼,思考一下官方的同學(xué)是怎么基于 Golang 設(shè)計出一個雙向鏈表的實現(xiàn)。
container/list
list 是 Golang 一經(jīng)推出就提供的能力,除了在 1.2 版本添加了 MoveAfter, MoveBefore 后,到目前為止就沒有別的迭代了。這一套實現(xiàn)是穩(wěn)定可靠的,而且源碼不過 240 行,沒有外部依賴,非常適合初學(xué)者練手。
我們來看看官方提供的使用示例:
import (
"container/list"
"fmt"
)
func main() {
// Create a new list and put some numbers in it.
l := list.New()
e4 := l.PushBack(4)
e1 := l.PushFront(1)
l.InsertBefore(3, e4)
l.InsertAfter(2, e1)
// Iterate through list and print its contents.
for e := l.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value)
}
}首先使用 list.New() 方法創(chuàng)建一個 List 對象,隨后就可以按照鏈表的正常邏輯去添加,刪除,移動 Element。最后不斷從 l.Front() 拿到數(shù)據(jù),并通過 Next() 迭代,就可以遍歷鏈表,輸出的結(jié)果如下:
1
2
3
4
整體來看,container/list 包封裝了兩個結(jié)構(gòu):
- List: 一個雙向鏈表
- Element: 一個鏈表中的元素
提供的能力也可以滿足大部分場景的需要:

下面我們來結(jié)合源碼看看 Element 和 List 是怎么實現(xiàn)的。
Element
// Element is an element of a linked list.
type Element struct {
// Next and previous pointers in the doubly-linked list of elements.
// To simplify the implementation, internally a list l is implemented
// as a ring, such that &l.root is both the next element of the last
// list element (l.Back()) and the previous element of the first list
// element (l.Front()).
next, prev *Element
// The list to which this element belongs.
list *List
// The value stored with this element.
Value any
}Element 的定義很簡單,包含了 4 個部分:
- 指向前一個元素的指針;
- 指向后一個元素的指針;
- 當(dāng)前元素所屬的 List;
- 存儲在當(dāng)前元素的值。
// Next returns the next list element or nil.
func (e *Element) Next() *Element {
if p := e.next; e.list != nil && p != &e.list.root {
return p
}
return nil
}
// Prev returns the previous list element or nil.
func (e *Element) Prev() *Element {
if p := e.prev; e.list != nil && p != &e.list.root {
return p
}
return nil
}作為雙向鏈表的節(jié)點,自然是需要有能力獲取到前一個元素,以及后一個元素。不過注意,這里并不是直接 return e.next 或者 return e.prev。
參考 Element 注釋我們知道:
To simplify the implementation, internally a list l is implemented as a ring, such that &l.root is both the next element of the last list element (l.Back()) and the previous element of the first list
container/list 中的 List 本質(zhì)是個環(huán)形結(jié)構(gòu)(ring),包含的 root 節(jié)點,既是雙向鏈表中最后一個元素的 next,也是第一個元素的 prev。
這個其實算法中很常見,本質(zhì)是個 sentinel node,或者叫【哨兵節(jié)點】,從 List 使用者的視角看是感知不到這個 root 存在的。多這一個節(jié)點,可以有效的幫助我們,操作頭結(jié)點和尾結(jié)點:

虛線框里的東西是真正存儲數(shù)據(jù)的【節(jié)點】,root 不存放數(shù)據(jù),只用于輔助我們實現(xiàn)這個雙向鏈表。
所以,這兒就很好理解了,當(dāng)我們實現(xiàn) Next() 方法時需要校驗,如果某個節(jié)點的 next 是 root,說明它就是最后一個了,所以應(yīng)該返回 nil(使用者是感知不到 root 的),Prev() 方法也是同理。
List
// List represents a doubly linked list.
// The zero value for List is an empty list ready to use.
type List struct {
root Element // sentinel list element, only &root, root.prev, and root.next are used
len int // current list length excluding (this) sentinel element
}
// Init initializes or clears list l.
func (l *List) Init() *List {
l.root.next = &l.root
l.root.prev = &l.root
l.len = 0
return l
}
// New returns an initialized list.
func New() *List { return new(List).Init() }
// Len returns the number of elements of list l.
// The complexity is O(1).
func (l *List) Len() int { return l.len }有了前面 root 的推理,這里看 List 就容易多了,和預(yù)期一樣,一個雙向鏈表對外只需提供一個節(jié)點即可,使用者可以調(diào)用 List 的 API 接口進行插入,刪除,調(diào)整順序,獲取元素。
這里 List 結(jié)構(gòu)只包含一個 root 元素,并且維護了一個 len 變量,記錄當(dāng)前鏈表的長度。我們通過 New() 構(gòu)建出的 List 對象,只包含 root 一個元素,所以它的 next 和 prev 都是自己。
獲取頭尾結(jié)點
// Front returns the first element of list l or nil if the list is empty.
func (l *List) Front() *Element {
if l.len == 0 {
return nil
}
return l.root.next
}
// Back returns the last element of list l or nil if the list is empty.
func (l *List) Back() *Element {
if l.len == 0 {
return nil
}
return l.root.prev
}Front 和 Back 兩個方法支持我們獲取鏈表的【頭結(jié)點】和【尾結(jié)點】,有了 root 節(jié)點的輔助,這一點也很容易。root 的下一個節(jié)點就是頭,root 的前一個節(jié)點就是尾,看一下我們上面畫的圖大家就很容易理解了。
基礎(chǔ)鏈表操作
這里我們來看幾個底層的鏈表操作,這幾個方法會支撐起來 container/list 對外的 API 實現(xiàn):
insert
// insert inserts e after at, increments l.len, and returns e.
func (l *List) insert(e, at *Element) *Element {
e.prev = at
e.next = at.next
e.prev.next = e
e.next.prev = e
e.list = l
l.len++
return e
}
// insertValue is a convenience wrapper for insert(&Element{Value: v}, at).
func (l *List) insertValue(v any, at *Element) *Element {
return l.insert(&Element{Value: v}, at)
}insert 方法接收 e, at 兩個 Element 指針入?yún)?,它的語義是將 e 元素掛在 at 元素之后。這里的步驟拆解一下:
調(diào)整 e 的前驅(qū)和后繼:
- 讓 e 的 prev 是 at;
- 讓 e 的 next 是 at 的 next。
調(diào)整 at 和 at 的 next 的指針,讓 e 作為中間節(jié)點:
- 讓 e 的 prev(也就是 at)的 next 指向 e;
- 讓 e 的 next(也就是原來 at 的 next)的 prev 指向 e。
將當(dāng)前的 List 賦值給 e 的 list,調(diào)整長度即可。
官方還提供了一個 insertValue 作為一個簡單的裝飾器,這樣可以直接傳入新節(jié)點的值即可,構(gòu)造節(jié)點這一步在這個方法內(nèi)發(fā)生。
remove
// remove removes e from its list, decrements l.len
func (l *List) remove(e *Element) {
e.prev.next = e.next
e.next.prev = e.prev
e.next = nil // avoid memory leaks
e.prev = nil // avoid memory leaks
e.list = nil
l.len--
}刪除節(jié)點很簡單,其實就是上面 insert 的逆向操作,找到要刪除的節(jié)點 e 的前驅(qū)和后繼,讓它的 prev 的 next 跳過它,指向更下一個節(jié)點,讓它的 next 的 prev 也跳過它,指向更前一個節(jié)點即可。最后把 len 更新一下。
需要注意的是,很多同學(xué)會犯錯誤,覺得調(diào)整完前驅(qū)后繼指針即可,但其實,按照 GC 語言的特性,雖然邏輯上這個雙向鏈表已經(jīng)沒有 e 了,但你沒有把 e 的 next 和 prev 指針清空,就會導(dǎo)致隨后它們指向的元素有可能不會被垃圾回收,導(dǎo)致出現(xiàn)內(nèi)存泄漏。
而內(nèi)存泄露的問題在線上是很難快速排查的,所以官方也是增加了 e.next = nil 和 e.prev = nil 這樣保證 GC 掃描的時候不會漏掉。
move
// move moves e to next to at.
func (l *List) move(e, at *Element) {
if e == at {
return
}
e.prev.next = e.next
e.next.prev = e.prev
e.prev = at
e.next = at.next
e.prev.next = e
e.next.prev = e
}move 和 insert 比較像,它的語義多一層,代表將 e 從原來位置挪走,放到 at 的后面。而 insert 是原先 e 不存在于這個鏈表,新加進來的。
所以,你會發(fā)現(xiàn)這里多了一步處理:
e.prev.next = e.next e.next.prev = e.prev
這兩行就是為了處理 e 原來所在位置的前驅(qū)和后繼,讓它們跳過 e,指向更前或更后的節(jié)點。
后面的 e 和 at 指針的調(diào)整和 insert 是完全對齊的,大家可以看一下。這里因為是 move,不是新增節(jié)點,所以也無需調(diào)整 len。
API 實現(xiàn)
有了上面三個基礎(chǔ)能力:insert, remove, move。配合上 List 的 root 節(jié)點,我們就可以隨心所欲在雙向鏈表里進行操作了。這里封裝的對外 API 很多,但都是基于上面我們提到的能力。
此處我們就不一一贅述了,大家感受一下即可:
// Remove removes e from l if e is an element of list l.
// It returns the element value e.Value.
// The element must not be nil.
func (l *List) Remove(e *Element) any {
if e.list == l {
// if e.list == l, l must have been initialized when e was inserted
// in l or l == nil (e is a zero Element) and l.remove will crash
l.remove(e)
}
return e.Value
}
// PushFront inserts a new element e with value v at the front of list l and returns e.
func (l *List) PushFront(v any) *Element {
l.lazyInit()
return l.insertValue(v, &l.root)
}
// PushBack inserts a new element e with value v at the back of list l and returns e.
func (l *List) PushBack(v any) *Element {
l.lazyInit()
return l.insertValue(v, l.root.prev)
}
// InsertBefore inserts a new element e with value v immediately before mark and returns e.
// If mark is not an element of l, the list is not modified.
// The mark must not be nil.
func (l *List) InsertBefore(v any, mark *Element) *Element {
if mark.list != l {
return nil
}
// see comment in List.Remove about initialization of l
return l.insertValue(v, mark.prev)
}
// InsertAfter inserts a new element e with value v immediately after mark and returns e.
// If mark is not an element of l, the list is not modified.
// The mark must not be nil.
func (l *List) InsertAfter(v any, mark *Element) *Element {
if mark.list != l {
return nil
}
// see comment in List.Remove about initialization of l
return l.insertValue(v, mark)
}
// MoveToFront moves element e to the front of list l.
// If e is not an element of l, the list is not modified.
// The element must not be nil.
func (l *List) MoveToFront(e *Element) {
if e.list != l || l.root.next == e {
return
}
// see comment in List.Remove about initialization of l
l.move(e, &l.root)
}
// MoveToBack moves element e to the back of list l.
// If e is not an element of l, the list is not modified.
// The element must not be nil.
func (l *List) MoveToBack(e *Element) {
if e.list != l || l.root.prev == e {
return
}
// see comment in List.Remove about initialization of l
l.move(e, l.root.prev)
}
// MoveBefore moves element e to its new position before mark.
// If e or mark is not an element of l, or e == mark, the list is not modified.
// The element and mark must not be nil.
func (l *List) MoveBefore(e, mark *Element) {
if e.list != l || e == mark || mark.list != l {
return
}
l.move(e, mark.prev)
}
// MoveAfter moves element e to its new position after mark.
// If e or mark is not an element of l, or e == mark, the list is not modified.
// The element and mark must not be nil.
func (l *List) MoveAfter(e, mark *Element) {
if e.list != l || e == mark || mark.list != l {
return
}
l.move(e, mark)
}
// PushBackList inserts a copy of another list at the back of list l.
// The lists l and other may be the same. They must not be nil.
func (l *List) PushBackList(other *List) {
l.lazyInit()
for i, e := other.Len(), other.Front(); i > 0; i, e = i-1, e.Next() {
l.insertValue(e.Value, l.root.prev)
}
}
// PushFrontList inserts a copy of another list at the front of list l.
// The lists l and other may be the same. They must not be nil.
func (l *List) PushFrontList(other *List) {
l.lazyInit()
for i, e := other.Len(), other.Back(); i > 0; i, e = i-1, e.Prev() {
l.insertValue(e.Value, &l.root)
}
}比如我們有的時候需要插入元素,但希望放到某個節(jié)點之前,這個時候類似 InsertBefore 的處理,直接拿到目標節(jié)點的 prev,插到它的 prev 之后,本質(zhì)上不就是放到了它之前了么?
l.insertValue(v, mark.prev)
這里的代碼都不復(fù)雜,建議大家有需要的時候?qū)φ諏崿F(xiàn)簡單了解即可。
完整示例
有了上面的源碼解析,我們來看看怎樣實用,這里我們附上了鏈表的狀態(tài)。從使用者的角度無需關(guān)心 root,其實還是很簡單基礎(chǔ)的鏈表能力,大家可以嘗試閱讀理解一下下面的調(diào)用結(jié)果:
package main
import (
"container/list"
"fmt"
)
func main() {
l := list.New()
l.PushBack("a")
printList(l) // a
l.PushBack("b")
printList(l) // a b
l.PushFront("c")
printList(l) // c a b
fmt.Println(l.Front().Value) // c
fmt.Println(l.Back().Value) // b
fmt.Println(l.Len()) // 3
l.MoveToBack(l.Front())
printList(l) // a b c
l.MoveToFront(l.Back())
printList(l) // c a b
l.Remove(l.Back())
printList(l) // c a
}
func printList(l *list.List) {
for e := l.Front(); e != nil; e = e.Next() {
fmt.Print(e.Value, " ")
}
fmt.Println()
}遍歷鏈表也很簡單,不斷從 front 拿到元素,賦值為 Next,持續(xù)下去,直到 Next 為 nil,說明遍歷結(jié)束,參照這里的 printList 即可。
結(jié)語
其實 container/list 不僅僅給我們展示了一個比較規(guī)范標準的雙向鏈表實現(xiàn),而且也廣泛應(yīng)用于很多業(yè)務(wù)場景,比如經(jīng)典的 groupcache 底層的 LRU 就是依靠 container/list 的能力,我們下一篇文章會分析一下。
以上就是深入了解Golang官方container/list原理的詳細內(nèi)容,更多關(guān)于Golang container/list的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go操作Kafka的實現(xiàn)示例(kafka-go)
本文介紹了使用kafka-go庫在Go語言中與Kafka進行交互,涵蓋了kafka-go的安裝、API使用、消息發(fā)送與消費方法,以及如何通過DockerCompose快速搭建Kafka環(huán)境,文章還比較了其他兩個常用的Kafka客戶端庫,感興趣的可以了解一下2024-10-10
淺析Go中fasthttp與net/http的性能對比及應(yīng)用
這篇文章主要為大家詳細介紹了Golang中fasthttp的底層實現(xiàn)以及與net/http的區(qū)別,下面就跟隨小編一起來看看fasthttp到底是如何做到性能如此之快的吧2024-03-03
詳解Golang利用反射reflect動態(tài)調(diào)用方法
這篇文章主要介紹了詳解Golang利用反射reflect動態(tài)調(diào)用方法,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-11-11
如何在golang中使用shopspring/decimal來處理精度問題
本文主要介紹了如何在golang中使用shopspring/decimal來處理精度問題,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04
解決golang處理http response碰到的問題和需要注意的點
這篇文章主要介紹了解決golang處理http response碰到的問題和需要注意的點,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12

