深入了解Golang官方container/list原理
開篇
我們繼續(xù)上一篇 解析 Golang 官方 container/heap 用法 學(xué)習(xí) Golang 的標(biāo)準(zhǔn)庫 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 的處理,直接拿到目標(biāo)節(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ī)范標(biāo)準(zhǔn)的雙向鏈表實現(xiàn),而且也廣泛應(yīng)用于很多業(yè)務(wù)場景,比如經(jīng)典的 groupcache 底層的 LRU 就是依靠 container/list 的能力,我們下一篇文章會分析一下。
以上就是深入了解Golang官方container/list原理的詳細(xì)內(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)用
這篇文章主要為大家詳細(xì)介紹了Golang中fasthttp的底層實現(xiàn)以及與net/http的區(qū)別,下面就跟隨小編一起來看看fasthttp到底是如何做到性能如此之快的吧2024-03-03詳解Golang利用反射reflect動態(tài)調(diào)用方法
這篇文章主要介紹了詳解Golang利用反射reflect動態(tài)調(diào)用方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-11-11如何在golang中使用shopspring/decimal來處理精度問題
本文主要介紹了如何在golang中使用shopspring/decimal來處理精度問題,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04解決golang處理http response碰到的問題和需要注意的點
這篇文章主要介紹了解決golang處理http response碰到的問題和需要注意的點,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12