It is true that Go is designed with a minimalistic touch. It does not have a rich set of collections or in-built data structures like Java,
where you can just import java.util.PriorityQueue
or java.util.Stack
and start using them.
However, besides the basic map
and slice
, Go does come with a builtin container
package that we should at least be aware and
make use of it to implement things like Stack
, Queue
, PriorityQueue
and Deque
.
container/list
Go has a doubly linked list implementation. Each node looks like:
type Element struct {
prev, next *Element
Value any
}
Most fundamentally, it supports to append and peek the Element
from the tail of the list, which means we can implement a Stack:
import "container/list"
type Stack struct {
data *list.List
}
func NewStack() *Stack {
return &Stack{
data: list.New(),
}
}
func (s *Stack) Push(value any) {
s.data.PushBack(value) // push element in from the back i.e. append
}
func (s *Stack) Pop() any {
elem := s.data.Back()
return s.data.Remove(elem)
}
func (s *Stack) Top() any {
elem := s.data.Back() // stack is first in, last out
return elem.Value
}
The List
also allows us to peek from the head. We can also build a queue from it:
type Queue struct {
data *list.List
}
func NewQueue() *Queue {
return &Queue{
data: list.New(),
}
}
// let's build a queue of integers
func (q *Queue) Add(val int) {
q.data.PushBack(val)
}
func (q *Queue) Poll() int {
elem := q.data.Front() // queue is first in, first out
if elem == nil {
return -1 // or whatever identifier that makes sense to the application, e.g. math.MinInt
}
value := q.data.Remove(elem)
return value.(int)
}
func (q *Queue) Peek() int {
elem := q.data.Front()
return elem.Value.(int)
}
func (q *Queue) Len() int {
return q.data.Len() // get the current number of elements in the queue
}
// what's more, we can print the current queue
func (q *Queue) Print() {
for e := q.data.Front(); e != nil; e = e.Next() {
fmt.Printf("%d ", e.Value)
}
fmt.Println()
}
func (q *Queue) Clear() {
q.data.Init() // clear all data in the queue; basically re-initialized the list
}
There is one more convenient function MoveToFront
that moves the specified element to the head of the list.
I find it really help when implementing an LRU cache. There is MoveToBack
available too, if you prefer.
type LRU struct {
data *list.List
m map[string]*list.Element
size int
}
func NewCache(capacity int) *LRU {
return &LRU{
data: list.New(),
m: make(map[string]*list.Element),
size: capacity,
}
}
func (c *LRU) Get(key string) any {
elem, ok := c.m[key]
if !ok {
return nil
}
// move the last visited element to the head of the list
c.data.MoveToFront(elem)
return e.Value
}
func (c *LRU) Put(key string, val any) {
if c.data.Len() >= c.size {
c.evict()
}
elem := c.data.PushFront(val)
c.m[key] = elem
}
func (c *LRU) evict() {
elem := c.data.Back() // remove the least recently visited element from the tail of the list
c.data.Remove(elem)
}
container/heap
Go also provides an interface to construct min heap, which is an ordered complete binary tree where each node is smaller than all its children.
package heap
type Interface interface {
sort.Interface
Push(x any) // add x as element Len()
Pop() any // remove and return element Len() - 1.
}
And sort interface allows user to define sorting behaviours of custom collections.
package sort
type Interface interface {
// Returns the number of elements in the collection
Len() int
// Return true if element at index i is less than element at index j
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
}
Now we have the arsenal to try to implement a PriorityQueue
in Go.
type Stock struct {
Code string
Price int
}
type PriorityQueue struct {
stocks []*Stock
}
// implement sort.Interface
func (q PriorityQueue) Less(i, j int) bool {
return q.stocks[i].Price < q.stocks[j].Price
}
func (q PriorityQueue) Swap(i, j int) {
q.stocks[i], q.stocks[j] = q.stocks[j], q.stocks[i]
}
func (q PriorityQueue) Len() int {
return len(q.stocks)
}
// implement heap.Interface
func (q *PriorityQueue) Push(s any) {
item := x.(*Stock)
q.stocks = append(q.stocks, item)
}
func (q *PriorityQueue) Pop() any {
size := len(q.stocks)
if size == 0 {
return nil
}
item := q.stocks[size-1]
q.stocks = q.stocks[:size-1]
return item
}
If we run through an example, we will see that the Stocks are sorted by their respective prices in ascending order.
func main() {
pq := PriorityQueue{
stocks: make([]*Stock, 0),
}
list := []*Stock{
{
Code: "GOOG",
Price: 197,
},
{
Code: "SQ",
Price: 91,
},
{
Code: "TSLA",
Price: 462,
}
}
for _, s := range list {
pq.Push(s)
}
heap.Init(&pq) // build up the heap properties by heapifying the collection
count := pq.Len()
for i := 0; i < count; i++ {
s := heap.Pop(&pq).(*Stock)
fmt.Printf("code: %s, price: %d\n", s.Code, s.Price)
}
}
Below output is seen as expected
code: SQ, price: 91
code: GOOG, price: 197
code: TSLA, price: 462