Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
69: Implementing a Linked List in Go
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
Songand a descriptive error. - If the list has only one element, ensure both
HeadandTailare set back tonilafter removal. - Correctly decrement the
Sizecounter. - Ensure the
Headpointer is moved to the second node in the list.
There are no comments for now.