Skip to Content
Course content

69: Implementing a Linked List in Go

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Now, in a real production environment, you'd almost always reach for a slice in Go. Slices are fast, they're cache-friendly, and they're the "idiomatic" way to handle sequences. But there's a reason we're doing this: understanding how to manipulate pointers and manually link memory is a fundamental skill. It's the difference between just using a tool and actually understanding how the tool is built.

To keep this concrete, let's build a simple music playlist. We want to be able to add songs to the end of the list and then iterate through them to "play" them. This is a classic use case for a singly linked list.

Structuring our Song and Node

First, we need to define what we're actually storing. I'll start with a Song struct, and then wrap that inside a Node. The node is the magic part—it holds the data and a pointer to the next node in the sequence.

type Song struct {
    Title  string
    Artist string
}

type Node struct {
    Value Song
    Next  *Node
}

I'm using a pointer to Node for the Next field. If Next is nil, we've hit the end of the playlist. Simple enough.

Creating the Playlist Wrapper

I could just pass around a *Node as the head of my list, but that gets messy quickly. It's much cleaner to create a Playlist struct that manages the head and the tail. Keeping a reference to the tail allows us to append songs in O(1) time instead of traversing the whole list every time we add a track.

type Playlist struct {
    Head *Node
    Tail *Node
    Size int
}

The Logic of Appending Songs

Now for the Append method. The goal here is to take a Song, wrap it in a Node, and stick it at the end of the chain. I'll start by creating the new node first.

func (p *Playlist) Append(s Song) {
    newNode := &Node{Value: s}
    
    // Point the current tail to the new node
    p.Tail.Next = newNode
    
    // Update the tail to be the new node
    p.Tail = newNode
    p.Size++
}

Wait, I just crashed the program

If you ran that code right now with an empty playlist, you'd get a runtime panic: invalid memory address or nil pointer dereference. I did exactly that a second ago. Why? Because when the playlist is empty, p.Tail is nil, and you can't set .Next on a nil pointer.

This is the classic linked list trap. We have to handle the "first element" case separately. If the head is nil, the first node we add becomes both the head and the tail.

func (p *Playlist) Append(s Song) {
    newNode := &Node{Value: s}
    
    if p.Head == nil {
        p.Head = newNode
        p.Tail = newNode
        p.Size++
        return
    }
    
    p.Tail.Next = newNode
    p.Tail = newNode
    p.Size++
}

Traversing the List to Play Songs

Finally, we need a way to actually see our list. To do this, we start at the Head and follow the Next pointers until we hit nil. I'll use a for loop here—it's the most natural way to handle this in Go.

func (p *Playlist) Display() {
    current := p.Head
    for current != nil {
        fmt.Printf("Playing: %s by %s\n", current.Value.Title, current.Value.Artist)
        current = current.Next
    }
}

Notice how I created a local current variable. Never move the p.Head pointer itself during traversal, or you'll permanently lose the start of your list and create a massive memory leak.




📋 Practical Task

Implement a "RemoveFirst" Method for the Playlist

Your task is to extend the Playlist implementation. Add a method called RemoveFirst() (Song, error) that removes the first song from the playlist and returns it to the caller.

Requirements:

  • If the list is empty, return an empty Song and a descriptive error.
  • If the list has only one element, ensure both Head and Tail are set back to nil after removal.
  • Correctly decrement the Size counter.
  • Ensure the Head pointer is moved to the second node in the list.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.