Java has a rich set of built-in container classes, and different containers are used to handle various business scenarios. Although Go has many similarities with Java in language design, it doesn’t support many container data structures natively, only map and slice. The standard library’s container package extends the container data structure to support three containers: Heap, LinkedList and Circular List.

Containers

Familiarity with C++ and Java should give you a clear understanding of containers, which are an integral part of modern programming practice and take many forms, generally described as objects with methods to manipulate the contents of the container.

  • Built-in containers.
    • map: Associated containers
    • slice: dynamically expanding sequential container
  • channels: queues
  • container standard library (pkg/container):
    • list: linkedlist container
    • ring: circular List container
    • heap: heap container, provides implementation of heap

slice, map and channel are the most common and built-in container data structures in Go, while all other containers are in the container package of the standard library. When using the container three containers, you don’t have to bother implementing algorithms related to data structures. Also, since all the input types supported by the containers provided by container are interface{}, you can handle any type of value as long as you implement the container’s interface.

container/list

The code for the linked list container list is an implementation of a Doubly Linked List. list maintains two constructs: Element and List.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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 interface{}
    }

// 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
}

go container/list

When a list is created with list.New(), an Element is initialized as a Root Pointer, which either points to the initial element of the list or is nil. In addition to the data field Value, each Element has prev and next that point to the direct predecessor and direct successor, respectively, to allow the user to move elements back and forth in the list.

The methods supported by the list container are as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
type Element
    func (e *Element) Next() *Element
    func (e *Element) Prev() *Element
type List
    func New() *List
    func (l *List) Back() *Element   // 最后一个元素
    func (l *List) Front() *Element  // 第一个元素
    func (l *List) Init() *List  // 链表初始化
    func (l *List) InsertAfter(v interface{}, mark *Element) *Element // 在某个元素后插入
    func (l *List) InsertBefore(v interface{}, mark *Element) *Element  // 在某个元素前插入
    func (l *List) Len() int // 在链表长度
    func (l *List) MoveAfter(e, mark *Element)  // 把 e 元素移动到 mark 之后
    func (l *List) MoveBefore(e, mark *Element)  // 把 e 元素移动到 mark 之前
    func (l *List) MoveToBack(e *Element) // 把 e 元素移动到队列最后
    func (l *List) MoveToFront(e *Element) // 把 e 元素移动到队列最头部
    func (l *List) PushBack(v interface{}) *Element  // 在队列最后插入元素
    func (l *List) PushBackList(other *List)  // 在队列最后插入接上新队列
    func (l *List) PushFront(v interface{}) *Element  // 在队列头部插入元素
    func (l *List) PushFrontList(other *List) // 在队列头部插入接上新队列
    func (l *List) Remove(e *Element) interface{} // 删除某个元素

The following is a simple example of a list.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

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.Printf(".2d",e.Value)
    }

}

The output of this code is: 1234. After initializing the list, insert 4 at the end node and 1 at the head node; then insert 3 before the 4 and 2 after the 1, so the result is 1234.

The time complexity of list in inserting and deleting data is O(1); random lookup is less efficient, O(N) (slice random lookup has a time efficiency of O(1)).

A common application scenario for linkedlist containers is for doing LRU caching.

container/ring

The circular list container ring is a linkedlist with no head and tail nodes, and can be seen here as a simplified version of a list.

ring maintains a structure Ring.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// A Ring is an element of a circular list, or ring.
// Rings do not have a beginning or end; a pointer to any ring element
// serves as reference to the entire ring. Empty rings are represented
// as nil Ring pointers. The zero value for a Ring is a one-element
// ring with a nil Value.
//
type Ring struct {
    next, prev *Ring
    Value      interface{} // for use by client; untouched by this library
}

ring

But unlike the list, the functions of the ring are not the same.

1
2
3
4
5
6
7
8
9
type Ring
    func New(n int) *Ring  // 初始化环
    func (r *Ring) Do(f func(interface{}))  // 循环环进行操作
    func (r *Ring) Len() int // 环长度
    func (r *Ring) Link(s *Ring) *Ring // 连接两个环
    func (r *Ring) Move(n int) *Ring // 指针从当前元素开始向后移动或者向前(n 可以为负数)
    func (r *Ring) Next() *Ring // 当前元素的下个元素
    func (r *Ring) Prev() *Ring // 当前元素的上个元素
    func (r *Ring) Unlink(n int) *Ring // 从当前元素开始,删除 n 个元素

Here is a simple example of a ring.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
    "container/ring"
    "fmt"
)

func main() {

    // Create a new ring of size 5
    r := ring.New(5)

    // Get the length of the ring
    n := r.Len()

    // Initialize the ring with some integer values
    for i := 0; i < n; i++ {
        r.Value = i
        r = r.Next()
    }

    // Iterate through the ring and print its contents
    r.Do(func(p interface{}) {
        fmt.Printf("%d", p.(int))
    })

}

The output of this code is 01234. After initializing the ring, the value of each element is assigned, and since the ring provides a Do method, the Print function is executed to print the result when the current element is traversed.

From the implementation of the ring, we know that the time complexity of the operation is O(N). Since ring nodes do not have head-to-tail distinction and FIFO characteristics, one application scenario where they can be used is ring buffers: providing mutually exclusive access to buffers without consuming resources.

container/heap

A heap is a complete binary tree implemented as an array. There are two types of heaps according to their characteristics: maximum heap and minimum heap, and the difference between them lies in the way the nodes are sorted.

  1. in the maximum heap, the value of the parent node is larger than the value of each child node, and the largest element of the heap is at the root node
  2. in the minimal heap, the value of the parent node is smaller than the value of each child node, and the smallest element of the heap is at the root node

Go’s heap container heap is implemented as a minimal heap, and the heap maintains an Interface.

1
2
3
4
5
6
7
8
// Note that Push and Pop in this interface are for package heap's
// implementation to call. To add and remove things from the heap,
// use heap.Push and heap.Pop.
type Interface interface {
    sort.Interface
    Push(x interface{}) // add x as element Len()
    Pop() interface{}   // remove and return element Len() - 1.
}

Interface inlines sort.Interface, which implements three methods in addition to the out-of-heap method Push() and the in-heap method push().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// A type, typically a collection, that satisfies sort.Interface can be
// sorted by the routines in this package. The methods require that the
// elements of the collection be enumerated by an integer index.
type Interface interface {
    // Len is the number of elements in the collection.
    Len() int
    // Less reports whether the element with
    // index i should sort before the element with index j.
    Less(i, j int) bool
    // Swap swaps the elements with indexes i and j.
    Swap(i, j int)

container/heap

As long as the data type of the Interface method is implemented, the minimum heap condition is met.

1
!h.Less(j, i) for 0 <= i < h.Len() and 2*i+1 <= j <= 2*i+2 and j < h.Len()

The heap.Init() function constructs a minimal heap (a slice sorted by precedence traversal), and the internal implementation of up() and down() adjusts the heap upward and downward, respectively.

  • Push(): When inserting an element into the heap, the element is inserted into the last node of the rightmost subtree, and then up() is called to go up to ensure the minimum heap.
  • Pop() : When pushing an element out of the heap, first swap the element with the last node of the right subtree, then pop the last node, and then call down() on root to guarantee the minimum heap.

The methods supported by the heap container are as follows.

1
2
3
4
5
func Fix(h Interface, i int) // 在 i 位置更新数据,重建最小堆
func Init(h Interface) // 初始化,把 h 构建成最小堆 
func Pop(h Interface) interface{} // 出堆操作
func Push(h Interface, x interface{}) // 入堆操作
func Remove(h Interface, i int) interface{} // 移除第 i 个元素

The following is an example of using a heap container to construct a priority queue pq.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package main

import (
    "container/heap"
    "fmt"
)

type PriorityQueue []*Item

func (pq PriorityQueue) Len() int           { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool { return pq[i].priority > pq[j].priority }

func (pq PriorityQueue) Swap(i, j int) {
    pq[i], pq[j] = pq[j], pq[i]
    pq[i].index = i
    pq[j].index = j
}

func (pq *PriorityQueue) Push(x interface{}) {
    n := len(*pq)
    item := x.(*Item)
    item.index = n
    *pq = append(*pq, item)
}

func (pq *PriorityQueue) Pop() interface{} {
    old := *pq
    n := len(old)
    item := old[n-1]
    old[n-1] = nil  // avoid memory leak
    item.index = -1 // for safety
    *pq = old[0 : n-1]
    return item
}

func (pq *PriorityQueue) update(item *Item, value string, priority int) {
    item.value = value
    item.priority = priority
    heap.Fix(pq, item.index)
}

func main() {
    items := map[string]int{
        "banana": 3,
        "apple":  2,
        "pear":   4,
    }

    pq := make(PriorityQueue, len(items))
    i := 0
    for value, priority := range items {
        pq[i] = &Item{
            value:    value,
            priority: priority,
            index:    i,
        }
        i++
    }
    heap.Init(&pq)

    // insert a new item
    item := &Item{
        value:    "orange",
        priority: 1,
    }
    heap.Push(&pq, item)
    pq.update(item, item.value, 5)

    fmt.Printf("\nheap length: %d\n", len(pq))
    for pq.Len() > 0 {
        i := heap.Pop(&pq).(*Item)
        fmt.Printf("%.2d:%s\t", i.priority, i.value)
    }
    fmt.Printf("\nheap length: %d\n", len(pq))
}

The output of this code is 05:orange 04:pear 03:banana 02:apple. The first thing is to define a PriorityQueue array of structures as a queue, with a priority field identifying the priority, comparing the priority of two elements in the Less() method, and the queue update() for updating the priority of the elements. Then each time when executing heap.Pop(&pq). (*Item) operation, the element with high priority in the largest heap will be taken out of the heap.

The heap is initialized with a time complexity of O(N); the time complexity of entering the heap, exiting the heap, moving elements and rebuilding the minimum heap is O(logN).

Heap containers are more common in practice and are often used in production to implement priority queues and high performance timers.

Summary

In this article, we’ve sorted out Go’s container data structures, analyzed the three containers implemented by the container package in the standard library: list, ring and the more complex heap, and introduced their implementation, features and usage scenarios. Although slice and map are often used as containers in Go, if you find that these two data structures do not meet our needs in actual development, you can search under pkg/container to see if there are any available data structures.