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
174: Generic Data Structures: Stack, Queue, LinkedList
I've been thinking about how we handle state in our applications. Often, we need to store things in a specific order—last-in first-out for undo buffers, or first-in first-out for background task processing. In the past, I'd just use a slice and call it a day, but as soon as you need a "proper" data structure that enforces these rules across different types of data, you hit a wall.
The redundancy trap
Let's say I'm building a simple Undo system for a text editor. I only need to store strings for now. I'll write a quick Stack.
type StringStack []string
func (s *StringStack) Push(v string) {
*s = append(*s, v)
}
func (s *StringStack) Pop() string {
if len(*s) == 0 {
return ""
}
res := (*s)[len(*s)-1]
*s = (*s)[:len(*s)-1]
return res
}
That works perfectly for strings. But then my teammate tells me the Undo system also needs to track "Action" objects—complex structs containing coordinates and timestamps. Now I'm staring at the code, realizing I have to copy-paste that entire logic just to change string to Action. It's tedious and feels wrong. I don't want two stacks; I want one idea of a stack that works for anything.
Generalizing the Stack
This is where generics come in. Instead of pinning the stack to a string, I'll use a type parameter. I'll call it T, which is the convention, and tell Go that T can be any type.
type Stack[T any] []T
func (s *Stack[T]) Push(v T) {
*s = append(*s, v)
}
func (s *Stack[T]) Pop() T {
if len(*s) == 0 {
var zero T // I need a way to return the "zero value" of T
return zero
}
res := (*s)[len(*s)-1]
*s = (*s)[:len(*s)-1]
return res
}
Now I can do s := Stack[string]{} or s := Stack[Action]{}. Notice that var zero T trick? Since I don't know what T is, I can't just return "" or 0. Declaring a variable without initializing it gives me the default zero value for whatever type T ends up being. It's a clean way to handle empty stacks.
The Queue performance wall
Next, I need a Queue for a print job processor. A Queue is just a Stack's mirror: first-in, first-out. I tried using a slice again, like this:
type Queue[T any] []T
func (q *Queue[T]) Enqueue(v T) {
*q = append(*q, v)
}
func (q *Queue[T]) Dequeue() T {
if len(*q) == 0 {
var zero T
return zero
}
res := (*q)[0]
*q = (*q)[1:] // Here's the problem
return res
}
It looks fine, right? But I remember a performance profile from a previous project. When you slice the front of a very large slice ((*q)[1:]), you aren't actually freeing the memory of the first element immediately, and if you do this millions of times, you can end up with a memory leak or unnecessary overhead. For a small tool, it's fine. For a production-grade queue, I want something more stable. I want a Linked List.
Wiring up a Generic Linked List
A Linked List is the foundation for a more efficient Queue because adding and removing from the ends is just a matter of updating pointers. But I can't just have a "List"; I need "Nodes".
type Node[T any] struct {
Value T
Next *Node[T]
}
type LinkedList[T any] struct {
Head *Node[T]
Tail *Node[T]
Size int
}
func (ll *LinkedList[T]) Append(v T) {
newNode := &Node[T]{Value: v}
if ll.Head == nil {
ll.Head = newNode
ll.Tail = newNode
} else {
ll.Tail.Next = newNode
ll.Tail = newNode
}
ll.Size++
}
Now, if I want to implement my Queue using this LinkedList, the Dequeue operation becomes incredibly cheap. I just move the Head pointer to Head.Next. No slicing, no shifting elements in memory. I'm just updating a reference.
By combining these, I've moved from a rigid, type-specific implementation to a set of tools. I have a Stack[T] for my undo buffers, and a LinkedList[T] that I can use to build a high-performance Queue[T] for my task processor. The beauty of generics here isn't just avoiding copy-pasting; it's that the logic for "how a list works" is now completely decoupled from "what the list holds."
📋 Practical Task
Implement a Generic Double-Ended Queue (Deque)
You need to expand the LinkedList[T] concept to create a Deque (Double-Ended Queue). A Deque allows you to push and pop from both the front and the back.
- Modify the
Node[T]struct to be doubly-linked (add aPrev *Node[T]field). - Implement a
Deque[T]struct that tracks both theHeadandTail. - Write four methods:
PushFront(v T),PushBack(v T),PopFront() T, andPopBack() T. - Ensure that
PopFrontandPopBackcorrectly handle cases where the Deque is empty or contains only one element.
Test your Deque by using it to store a sequence of int values and then a sequence of string values to verify the generics are working correctly.
There are no comments for now.