What is a linked list?

According to the Almighty Wikipedia, a linked list is a linear collection of data elements whose order is not given by their physical placement in memory. Instead, each element points to the next. So they are similar to arrays, but allow for more flexible memory location. Arrays must have contiguous memory, while linked lists allow memory to be spread out. Each node in the list will provide a pointer to where the next node resides in memory so you can find it by running down the chain.

How do we build one?

Initial boilerplate

Let’s start by creating a file called main.go. In it, we will paste this basic code to get us up and running:

package main

import (
    "errors"
    "log"
)

func main() {
    // Optionally parse flags here

    if err := run(); err != nil {
        log.Fatalln(err)
    }
}

We will define the run function later.

Structs

Every good data structure needs structs. Here, we need two:

type node struct {
    next *node
    elem string
}

type linkedlist struct {
    head *node
    tail *node
    size int
}

The node struct has a pointer to the next node as it’s first member. By default, node.next will be nil, which is fine. We will link the nodes by adding nodes to the next variable. The elem variable is the actual data we care about. This can be of any type, we are just using strings to keep it simple.

The linkedlist struct is the actual meat and potatoes of everything. All of the methods we are about to define will come from this struct. As you can probably guess, head is the first node in the list, tail is the last, and size holds the count of how many nodes are in the list.

Methods

Methods adds functionality to our structs. There are many different possible methods we could use, but at its core, a linked list needs the following:

func (l *linkedlist) push(elem string) {
    n := &node{
        elem: elem,
    }

    if l.size == 0 {
        l.head = n
        l.tail = n
    } else {
        l.tail.next = n
        l.tail = n
    }

    l.size++
}

func (l *linkedlist) at(index int) (string, error) {
    if index >= l.size {
        return "", errors.New("Index out of bounds")
    }

    node := l.head
    for i := 0; i < index; i++ {
        node = node.next
    }

    return node.elem, nil
}

The first function, push, is used to add a new element to the end of the list. I could call the function append, but that’s already keyword in golang, so we’ll call it push. Later, we will see data structures that have push and pop. First, push creates a node that holds the elem we were given. Then it does two different things depending on the size of the list. If the list is empty (l.size == 0), we simply point head and tail to the new node. If the list already has something in it, we tell the last node to point to the new node l.tail.next = n, then we set the new node to be the last node in the list l.tail = n. Order is important here. If you swap those two lines, you will get nil pointer errors. Then, finally, we increment the size by one since we just added a node.

The second function, at, simply returns the node at the specific index. This is 0-based indexing so calling at(0) will return the first element in the list. We have to do bounds checking, which is why we can return an error, but otherwise we simply return the element of the node at that index. Looking at the code now, we see that the first if statement does the aforementioned bounds-checking, then we simply loop through the nodes. This is the ’linked’ part of ’linked list’. Here, the node variable is a pointer to a node in the list. We incrementally move this pointer down the chain by setting node = node.next. Once we iterate the correct number of times, we return the element of the current node. As you can see, this data structure does NOT allow for random access. That means if we have a list with 2,000 elements, and we want to find the 1,000th element, we need to loop over the 0th element, then the 1st, then the 2nd, etc. So the at function grows linearly with the size of the list.

Constructor

I know golang doesn’t have constructors per se, but we can still define a function that returns a linked list so we don’t have to repeat ourselves.

func NewLinkedList(elems ...string) *linkedlist {
    ll := &linkedlist{}

    for _, v := range elems {
        ll.push(v)
    }

    return ll
}

First, we create an empty linkedlist. This allows to reuse the methods that are defined for that structure. Namely, we call push once per element in the elems slice. Then we simply return a pointer to the list.

Usage

The run function is where we get to use the linkedlist.

func run() error {
    ll := NewLinkedList("foo", "bar")

    log.Println(ll.at(0)) // "foo" nil
    log.Println(ll.at(1)) // "bar" nil
    log.Println(ll.at(2)) // ""    "Index out of bounds"

    ll.push("baz")
    log.Println(ll.at(2)) // "baz" nil

    return nil
}

You can see we create a linkedlist with two elements, then we print out those two elements. We also try to print the element at index 2, but we get an error. So we simply append a new element and then we can print out that element. It’s worth noting that you don’t need to know anything about a node to use a linkedlist. If we were putting this into a library, we could keep node as unexported and only export the linkedlist and its associated methods. We don’t even need to export the fields on a linkedlist. This dramatically cleans up the code.

Improvements

In the above example, we create a single linked list that can add and retrieve elements. In order to be CRUD complete, we still need to be able to update an element as well as delete it. There are even some scenarios where we need have pointers going forward and backwards in what’s called a doubly linked list. That will be as simple as changing the node type to be:

type node struct {
    prev *node
    next *node
    elem string
}