
Mastering DSA in Go: From Basics to Advanced Patterns
- Sarvesh Mishra
- Programming, Go, Algorithms
- 07 May, 2026
Introduction
Go has rapidly become the language of choice for building robust, scalable backend infrastructure. But beyond building microservices and orchestrating containers, Go is an exceptionally powerful language for learning and applying Data Structures & Algorithms (DSA). Its simplicity, combined with a pragmatic standard library, makes it a formidable tool for both competitive programming and technical interviews.
One of Go's greatest strengths in DSA is its readable, self-documenting syntax. You spend less time deciphering complex language features and more time focusing on the underlying algorithmic logic. Built-in primitives like slices, maps, and goroutines map naturally to real-world DSA use cases, providing a smooth transition from theoretical concepts to practical implementation.
Furthermore, Go's strong standard library—specifically packages like sort, container/heap, and container/list—reduces the boilerplate often required in other systems languages. Static typing catches edge cases and bugs early in the compilation process, meaning fewer runtime surprises during crucial moments like coding contests or high-pressure FAANG-style interviews.
Uniquely, Go's lightweight goroutines and channels unlock concurrent algorithm design that is cumbersome or downright impossible in many other languages. Given that Go is widely used in massive backend infrastructure projects like Docker and Kubernetes, mastering DSA in Go translates directly to writing better, more efficient production code. If you are a developer looking to sharpen your algorithmic thinking while leveling up your Go skills, you are in the right place.
SECTION 1 — Getting Started with Go for DSA
Before diving into complex structures, setting up an efficient Go workspace and understanding its unique idioms is crucial for a smooth DSA practice experience.
Setting Up Your Go Workspace
A clean workspace allows you to switch between problems quickly. Initialize a new Go module for your DSA practice:
mkdir go-dsa && cd go-dsa
go mod init github.com/yourusername/go-dsa
Organize your folder structure by topic (e.g., arrays/, trees/, graphs/). This keeps your code modular and makes it easy to review past solutions. Creating a shared utils/ package for common helper functions is also a great practice.
Essential Go Idioms for DSA
Go's design choices heavily influence how you write algorithms. Understanding these idioms is non-negotiable:
- Zero Values: Go guarantees that uninitialized variables are given a useful "zero value" (e.g.,
0for ints,""for strings,nilfor pointers, slices, and maps). This eliminates a whole class of uninitialized variable bugs. - Slices vs. Arrays: In Go, arrays have a fixed size determined at compile time. Slices are dynamically sized, flexible views into the elements of an array. You will almost exclusively use slices for DSA.
make()andappend(): Usemake()to pre-allocate slices and maps when you know the capacity beforehand. This prevents expensive reallocation operations. Useappend()to dynamically add elements to a slice.
Efficient Input Reading
For competitive programming, standard fmt.Scan can be too slow for large inputs. Instead, use bufio.Scanner to read input efficiently.
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
// Time Complexity: O(N) where N is the number of integers
// Space Complexity: O(N) to store the input strings before parsing
func main() {
scanner := bufio.NewScanner(os.Stdin)
// Increase buffer capacity for very long lines
const maxCapacity = 1024 * 1024
buf := make([]byte, maxCapacity)
scanner.Buffer(buf, maxCapacity)
if scanner.Scan() {
line := scanner.Text()
parts := strings.Fields(line)
sum := 0
for _, p := range parts {
num, _ := strconv.Atoi(p)
sum += num
}
fmt.Println(sum)
}
}
Writing Reusable Utility Functions
Writing utility functions like min, max, and abs is a rite of passage in Go. While Go 1.21+ introduced built-in min and max functions, it's still useful to know how to write them, especially if you are stuck on an older version.
// Pre Go 1.21 generic implementation
func Min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
func Abs(x int) int {
if x < 0 {
return -x
}
return x
}
Testing Your Solutions
Go's built-in testing package is fantastic for validating DSA solutions. Create a file ending in _test.go and write table-driven tests to cover edge cases thoroughly.
// two_sum_test.go
func TestTwoSum(t *testing.T) {
tests := []struct {
nums []int
target int
want []int
}{
{[]int{2, 7, 11, 15}, 9, []int{0, 1}},
{[]int{3, 2, 4}, 6, []int{1, 2}},
}
for _, tt := range tests {
got := TwoSum(tt.nums, tt.target)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("TwoSum(%v, %d) = %v; want %v", tt.nums, tt.target, got, tt.want)
}
}
}
SECTION 2 — Core Data Structures in Go
Mastering core data structures is the foundation of any algorithmic journey. Let's look at how to implement them idiomatically in Go.
Arrays & Slices
While arrays are fixed-size values in Go, slices are references to a contiguous segment of an array. A slice is essentially a struct containing a pointer to the underlying array, a length, and a capacity.
2D Slices: Go doesn't have true multi-dimensional arrays. Instead, you create a slice of slices.
// Rotating a matrix by 90 degrees clockwise
// Time Complexity: O(N^2) where N is the dimension of the matrix
// Space Complexity: O(1) in-place modification
func rotate(matrix [][]int) {
n := len(matrix)
// Transpose
for i := 0; i < n; i++ {
for j := i; j < n; j++ {
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
}
}
// Reverse rows
for i := 0; i < n; i++ {
for j, k := 0, n-1; j < k; j, k = j+1, k-1 {
matrix[i][j], matrix[i][k] = matrix[i][k], matrix[i][j]
}
}
}
Strings
In Go, a string is a read-only slice of bytes. To iterate over Unicode characters, you must iterate over runes (alias for int32).
For O(1) amortized string concatenation, always use strings.Builder. Avoid the + operator in loops, as strings are immutable and it will trigger O(N^2) memory allocations.
import "strings"
// Time Complexity: O(N) where N is the length of the words slice
// Space Complexity: O(N) for the resulting string
func buildSentence(words []string) string {
var builder strings.Builder
for i, word := range words {
builder.WriteString(word)
if i < len(words)-1 {
builder.WriteByte(' ')
}
}
return builder.String()
}
Linked Lists
You can implement singly and doubly linked lists easily using structs. Alternatively, Go provides a built-in doubly linked list in the container/list package.
// Singly Linked List Node
type ListNode struct {
Val int
Next *ListNode
}
// Reversing a linked list
// Time Complexity: O(N)
// Space Complexity: O(1)
func reverseList(head *ListNode) *ListNode {
var prev *ListNode
curr := head
for curr != nil {
next := curr.Next
curr.Next = prev
prev = curr
curr = next
}
return prev
}
Stacks & Queues
Slices are perfect for implementing stacks and queues. A stack pushes and pops from the end of the slice. A queue pushes to the end and pops from the beginning (though popping from the front of a slice is O(N)).
Queue using Two Stacks
A queue can be implemented using two stacks (slices). Push operations go to the in stack. For pop operations, if the out stack is empty, we transfer all elements from in to out, reversing their order.
// Queue using Two Stacks
// Time Complexity: O(1) amortized for push/pop
// Space Complexity: O(N)
type MyQueue struct {
in []int
out []int
}
func (q *MyQueue) Push(x int) {
q.in = append(q.in, x)
}
func (q *MyQueue) Pop() int {
if len(q.out) == 0 {
for len(q.in) > 0 {
q.out = append(q.out, q.in[len(q.in)-1])
q.in = q.in[:len(q.in)-1]
}
}
res := q.out[len(q.out)-1]
q.out = q.out[:len(q.out)-1]
return res
}
Monotonic Stack Pattern: Useful for finding the "next greater element".
// Next Greater Element using a Monotonic Stack
// Time Complexity: O(N)
// Space Complexity: O(N)
func nextGreaterElement(nums []int) []int {
res := make([]int, len(nums))
for i := range res {
res[i] = -1
}
stack := []int{} // stores indices
for i, num := range nums {
for len(stack) > 0 && nums[stack[len(stack)-1]] < num {
idx := stack[len(stack)-1]
stack = stack[:len(stack)-1] // Pop
res[idx] = num
}
stack = append(stack, i) // Push
}
return res
}
Hash Maps & Sets
Go provides built-in maps (map[K]V). A crucial "gotcha" is that writing to a nil map causes a panic; always initialize with make().
Go doesn't have a built-in Set, but you can idiomatically create one using map[T]struct{}. An empty struct consumes zero bytes of memory.
// Frequency Counting Pattern
// Time Complexity: O(N)
// Space Complexity: O(N)
func hasDuplicate(nums []int) bool {
seen := make(map[int]struct{})
for _, num := range nums {
if _, exists := seen[num]; exists {
return true
}
seen[num] = struct{}{}
}
return false
}
Heaps / Priority Queues
To implement a heap, define a type that satisfies the heap.Interface from the container/heap package. This requires implementing Len(), Less(), Swap(), Push(), and Pop().
import "container/heap"
// An IntHeap is a min-heap of ints.
type IntHeap []int
func (h IntHeap) Len() int { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *IntHeap) Push(x interface{}) {
*h = append(*h, x.(int))
}
func (h *IntHeap) Pop() interface{} {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}
// Top K Elements
// Time Complexity: O(N log K)
// Space Complexity: O(K)
func findKthLargest(nums []int, k int) int {
h := &IntHeap{}
heap.Init(h)
for _, num := range nums {
heap.Push(h, num)
if h.Len() > k {
heap.Pop(h)
}
}
return heap.Pop(h).(int)
}
SECTION 3 — Algorithms and Patterns
Recognizing patterns is the key to solving complex algorithmic problems. Let's explore essential patterns implemented in Go.
Sorting
Go's sort package is highly optimized. Use sort.Ints(), sort.Strings(), or sort.Slice() for custom comparators.
| Algorithm | Best Time | Avg Time | Worst Time | Space |
|---|---|---|---|---|
| Merge Sort | O(N log N) | O(N log N) | O(N log N) | O(N) |
| Quick Sort | O(N log N) | O(N log N) | O(N^2) | O(log N) |
| Counting Sort | O(N + K) | O(N + K) | O(N + K) | O(K) |
import "sort"
type Person struct {
Name string
Age int
}
// Time Complexity: O(N log N)
// Space Complexity: O(log N) or O(N) depending on sort implementation
func sortByAge(people []Person) {
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})
}
Searching
For binary search, sort.SearchInts is available, but manually implementing it provides more flexibility, especially for the "Binary Search on Answer" pattern.
// Minimum Days to Complete Tasks (Binary Search on Answer)
// Time Complexity: O(N log M) where M is the search space
// Space Complexity: O(1)
func minDays(weights []int, days int) int {
left, right := 0, 0
for _, w := range weights {
if w > left {
left = w
}
right += w
}
for left < right {
mid := left + (right-left)/2
if canFinish(weights, days, mid) {
right = mid
} else {
left = mid + 1
}
}
return left
}
func canFinish(weights []int, days, capacity int) bool {
daysNeeded, currentLoad := 1, 0
for _, w := range weights {
if currentLoad+w > capacity {
daysNeeded++
currentLoad = w
} else {
currentLoad += w
}
}
return daysNeeded <= days
}
Two Pointers
The Two Pointers pattern is elegant for traversing arrays or strings from both ends.
// 3Sum
// Time Complexity: O(N^2)
// Space Complexity: O(1) or O(N) depending on sort
import "sort"
func threeSum(nums []int) [][]int {
sort.Ints(nums)
var res [][]int
for i := 0; i < len(nums)-2; i++ {
if i > 0 && nums[i] == nums[i-1] {
continue // skip duplicates
}
left, right := i+1, len(nums)-1
for left < right {
sum := nums[i] + nums[left] + nums[right]
if sum == 0 {
res = append(res, []int{nums[i], nums[left], nums[right]})
left++
right--
for left < right && nums[left] == nums[left-1] {
left++
}
for left < right && nums[right] == nums[right+1] {
right--
}
} else if sum < 0 {
left++
} else {
right--
}
}
}
return res
}
// Container With Most Water
// Time Complexity: O(N)
// Space Complexity: O(1)
func maxArea(height []int) int {
left, right := 0, len(height)-1
maxWater := 0
for left < right {
h := height[left]
if height[right] < h {
h = height[right]
}
area := h * (right - left)
if area > maxWater {
maxWater = area
}
if height[left] < height[right] {
left++
} else {
right--
}
}
return maxWater
}
Sliding Window
Sliding Window is indispensable for subarray problems.
// Maximum Sum Subarray of Size K (Fixed Window)
// Time Complexity: O(N)
// Space Complexity: O(1)
func maxSumSubarray(arr []int, k int) int {
if len(arr) < k {
return 0
}
windowSum, maxSum := 0, 0
for i := 0; i < k; i++ {
windowSum += arr[i]
}
maxSum = windowSum
for i := k; i < len(arr); i++ {
windowSum += arr[i] - arr[i-k]
if windowSum > maxSum {
maxSum = windowSum
}
}
return maxSum
}
Prefix Sum & Difference Array
Prefix sums allow O(1) range queries after O(N) preprocessing. Difference arrays allow O(1) range updates.
// Prefix Sum for Range Queries
// Time Complexity: O(N) preprocessing, O(1) query
// Space Complexity: O(N)
type NumArray struct {
prefix []int
}
func Constructor(nums []int) NumArray {
prefix := make([]int, len(nums)+1)
for i := 0; i < len(nums); i++ {
prefix[i+1] = prefix[i] + nums[i]
}
return NumArray{prefix}
}
func (this *NumArray) SumRange(left int, right int) int {
return this.prefix[right+1] - this.prefix[left]
}
Recursion & Backtracking
When dealing with deep recursion in Go, keep an eye on stack memory. Goroutines start with a small stack (2KB), which grows dynamically.
// Generate Subsets (Backtracking)
// Time Complexity: O(N * 2^N)
// Space Complexity: O(N) for recursion stack
func subsets(nums []int) [][]int {
var res [][]int
var backtrack func(start int, current []int)
backtrack = func(start int, current []int) {
temp := make([]int, len(current))
copy(temp, current)
res = append(res, temp)
for i := start; i < len(nums); i++ {
current = append(current, nums[i])
backtrack(i+1, current)
current = current[:len(current)-1]
}
}
backtrack(0, []int{})
return res
}
SECTION 4 — Graph Algorithms
Graphs are ubiquitous in tech interviews and backend systems. Go's maps and slices handle them beautifully.
Representation
Adjacency lists are the go-to representation. Use [][]int if node values are sequential integers 0 to N-1, or map[int][]int for arbitrary identifiers.
Traversal: BFS and DFS
BFS uses a queue, while DFS uses recursion (call stack) or an explicit stack.
// BFS Traversal
// Time Complexity: O(V + E)
// Space Complexity: O(V)
func bfs(adjList [][]int, start int) []int {
visited := make(map[int]bool)
queue := []int{start}
visited[start] = true
var res []int
for len(queue) > 0 {
node := queue[0]
queue = queue[1:] // Dequeue
res = append(res, node)
for _, neighbor := range adjList[node] {
if !visited[neighbor] {
visited[neighbor] = true
queue = append(queue, neighbor)
}
}
}
return res
}
// Detecting Cycles in Undirected Graph (DFS)
// Time Complexity: O(V + E)
// Space Complexity: O(V)
func hasCycle(n int, edges [][]int) bool {
adj := make([][]int, n)
for _, e := range edges {
adj[e[0]] = append(adj[e[0]], e[1])
adj[e[1]] = append(adj[e[1]], e[0])
}
visited := make([]bool, n)
var dfs func(u, parent int) bool
dfs = func(u, parent int) bool {
visited[u] = true
for _, v := range adj[u] {
if !visited[v] {
if dfs(v, u) {
return true
}
} else if v != parent {
return true // Cycle detected
}
}
return false
}
for i := 0; i < n; i++ {
if !visited[i] && dfs(i, -1) {
return true
}
}
return false
}
Shortest Path: Dijkstra's Algorithm & Bellman-Ford
Dijkstra finds the shortest path from a source to all other nodes with non-negative weights, leveraging container/heap. For graphs with negative weights, Bellman-Ford is necessary.
// Bellman-Ford Algorithm
// Time Complexity: O(V * E)
// Space Complexity: O(V)
func bellmanFord(n int, edges [][]int, src int) []int {
dist := make([]int, n)
for i := range dist {
dist[i] = 1e9
}
dist[src] = 0
// Relax all edges V-1 times
for i := 0; i < n-1; i++ {
for _, e := range edges {
u, v, w := e[0], e[1], e[2]
if dist[u] != 1e9 && dist[u]+w < dist[v] {
dist[v] = dist[u] + w
}
}
}
// Optional: 1 more relaxation to detect negative weight cycles
return dist
}
Here is a Dijkstra implementation in Go:
import "container/heap"
type Item struct {
node, dist, index int
}
type PQ []*Item
func (pq PQ) Len() int { return len(pq) }
func (pq PQ) Less(i, j int) bool { return pq[i].dist < pq[j].dist }
func (pq PQ) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
pq[i].index = i
pq[j].index = j
}
func (pq *PQ) Push(x interface{}) {
n := len(*pq)
item := x.(*Item)
item.index = n
*pq = append(*pq, item)
}
func (pq *PQ) Pop() interface{} {
old := *pq
n := len(old)
item := old[n-1]
old[n-1] = nil
item.index = -1
*pq = old[0 : n-1]
return item
}
// Dijkstra's Algorithm
// Time Complexity: O((V + E) log V)
// Space Complexity: O(V)
func dijkstra(n int, edges [][]int, src int) []int {
adj := make(map[int][][2]int) // node -> [neighbor, weight]
for _, e := range edges {
u, v, w := e[0], e[1], e[2]
adj[u] = append(adj[u], [2]int{v, w})
}
dist := make([]int, n)
for i := range dist {
dist[i] = 1e9 // Infinity
}
dist[src] = 0
pq := make(PQ, 0)
heap.Init(&pq)
heap.Push(&pq, &Item{node: src, dist: 0})
for pq.Len() > 0 {
curr := heap.Pop(&pq).(*Item)
u := curr.node
d := curr.dist
if d > dist[u] {
continue
}
for _, neighbor := range adj[u] {
v, weight := neighbor[0], neighbor[1]
if dist[u]+weight < dist[v] {
dist[v] = dist[u] + weight
heap.Push(&pq, &Item{node: v, dist: dist[v]})
}
}
}
return dist
}
Advanced Graphs
Topological sort is vital for dependency resolution (Kahn's algorithm). Union-Find (Disjoint Set Union) is essential for connectivity queries and Minimum Spanning Trees (Kruskal's).
SECTION 5 — Dynamic Programming in Go
Dynamic Programming involves breaking down problems into overlapping subproblems and applying optimal substructure.
The DP Mindset
You can approach DP top-down (Memoization) or bottom-up (Tabulation).
2D DP Template and Optimization
Often, 2D DP can be optimized to 1D space if the current state only depends on the immediately previous row.
// 0/1 Knapsack (Space Optimized Tabulation)
// Time Complexity: O(N * W) where N is items, W is capacity
// Space Complexity: O(W)
func knapsack(weights []int, values []int, W int) int {
dp := make([]int, W+1)
for i := 0; i < len(weights); i++ {
// Iterate backwards to avoid using the same item multiple times
for w := W; w >= weights[i]; w-- {
if dp[w-weights[i]]+values[i] > dp[w] {
dp[w] = dp[w-weights[i]] + values[i]
}
}
}
return dp[W]
}
To identify a DP problem, look for clues asking for the "minimum", "maximum", "longest", "shortest", or "number of ways" to achieve something, combined with constraints that suggest making sequential decisions. If you can break the problem down into smaller instances of the same problem (optimal substructure) and you find yourself computing the same subproblems repeatedly (overlapping subproblems), it's highly likely a DP problem. (Note: other canonical problems like Matrix Chain Multiplication also fall under advanced DP patterns but are usually solved via similar tabular or memoized approaches).
Key DP Patterns and Implementations
Here are canonical DP problems implemented in Go, progressing from basic to advanced.
1. Fibonacci Numbers: A classic introductory DP problem. We optimize space from O(N) down to O(1) by only keeping track of the previous two states.
// Fibonacci (Space Optimized Tabulation)
// Time Complexity: O(N)
// Space Complexity: O(1)
func fib(n int) int {
if n <= 1 {
return n
}
prev2, prev1 := 0, 1
for i := 2; i <= n; i++ {
curr := prev1 + prev2
prev2 = prev1
prev1 = curr
}
return prev1
}
// Longest Common Subsequence
// Time Complexity: O(N * M)
// Space Complexity: O(N * M)
func longestCommonSubsequence(text1 string, text2 string) int {
n, m := len(text1), len(text2)
dp := make([][]int, n+1)
for i := range dp {
dp[i] = make([]int, m+1)
}
for i := 1; i <= n; i++ {
for j := 1; j <= m; j++ {
if text1[i-1] == text2[j-1] {
dp[i][j] = dp[i-1][j-1] + 1
} else {
if dp[i-1][j] > dp[i][j-1] {
dp[i][j] = dp[i-1][j]
} else {
dp[i][j] = dp[i][j-1]
}
}
}
}
return dp[n][m]
}
// Longest Increasing Subsequence (with Binary Search)
// Time Complexity: O(N log N)
// Space Complexity: O(N)
import "sort"
func lengthOfLIS(nums []int) int {
tails := []int{}
for _, x := range nums {
idx := sort.SearchInts(tails, x)
if idx == len(tails) {
tails = append(tails, x)
} else {
tails[idx] = x
}
}
return len(tails)
}
// Coin Change
// Time Complexity: O(N * amount)
// Space Complexity: O(amount)
func coinChange(coins []int, amount int) int {
dp := make([]int, amount+1)
for i := 1; i <= amount; i++ {
dp[i] = amount + 1
}
dp[0] = 0
for i := 1; i <= amount; i++ {
for _, coin := range coins {
if i-coin >= 0 && dp[i-coin]+1 < dp[i] {
dp[i] = dp[i-coin] + 1
}
}
}
if dp[amount] > amount {
return -1
}
return dp[amount]
}
// Palindrome Partitioning (Partition DP / Backtracking with Memoization)
// Time Complexity: O(N * 2^N)
// Space Complexity: O(N) for recursion stack
func partition(s string) [][]string {
var res [][]string
var path []string
isPalindrome := func(str string) bool {
l, r := 0, len(str)-1
for l < r {
if str[l] != str[r] {
return false
}
l++
r--
}
return true
}
var dfs func(start int)
dfs = func(start int) {
if start == len(s) {
temp := make([]string, len(path))
copy(temp, path)
res = append(res, temp)
return
}
for i := start; i < len(s); i++ {
if isPalindrome(s[start : i+1]) {
path = append(path, s[start:i+1])
dfs(i + 1)
path = path[:len(path)-1]
}
}
}
dfs(0)
return res
}
SECTION 6 — Concurrency and DSA
Go truly shines in its concurrency primitives. While rarely tested in standard DSA interviews, concurrent algorithm design is a critical skill for senior engineers.
Why Concurrency Matters in DSA
Algorithms like parallel merge sort, parallel graph traversal, or MapReduce patterns drastically reduce execution time on massive datasets by utilizing multiple cores.
Concurrent BFS / Parallel Graph Traversal
For massive graphs, standard BFS can be slow. We can parallelize traversal layer by layer using goroutines and sync.WaitGroup.
// Concurrent BFS (Layer by Layer)
// Time Complexity: O(V + E)
// Space Complexity: O(V)
func concurrentBFS(start int, adjList map[int][]int) {
visited := sync.Map{}
visited.Store(start, true)
currentLayer := []int{start}
for len(currentLayer) > 0 {
var wg sync.WaitGroup
var mu sync.Mutex
var nextLayer []int
for _, node := range currentLayer {
wg.Add(1)
go func(u int) {
defer wg.Done()
for _, v := range adjList[u] {
if _, seen := visited.LoadOrStore(v, true); !seen {
mu.Lock()
nextLayer = append(nextLayer, v)
mu.Unlock()
}
}
}(node)
}
wg.Wait() // Wait for entire layer to finish
currentLayer = nextLayer
}
}
Mutex-Safe Data Structures
To build thread-safe structures, wrap standard slice operations in a sync.Mutex.
// Thread-Safe Queue
// Time Complexity: O(1) for both Enqueue and Dequeue
// Space Complexity: O(N)
type SafeQueue struct {
items []int
mu sync.Mutex
}
func (q *SafeQueue) Enqueue(item int) {
q.mu.Lock()
defer q.mu.Unlock()
q.items = append(q.items, item)
}
func (q *SafeQueue) Dequeue() (int, bool) {
q.mu.Lock()
defer q.mu.Unlock()
if len(q.items) == 0 {
return 0, false
}
item := q.items[0]
q.items = q.items[1:]
return item, true
}
Concurrent Word Frequency Counter
Using goroutines, wait groups, and channels allows us to process data pipelines efficiently.
package main
import (
"fmt"
"strings"
"sync"
)
// Time Complexity: O(N) overall processing, split across workers
// Space Complexity: O(U) where U is the number of unique words
func concurrentWordCount(texts []string) map[string]int {
var wg sync.WaitGroup
wordChan := make(chan string, 100)
counts := make(map[string]int)
// Mutex to protect map writes
var mu sync.Mutex
// Worker Goroutines
workerCount := 4
for i := 0; i < workerCount; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for word := range wordChan {
mu.Lock()
counts[word]++
mu.Unlock()
}
}()
}
// Producer
for _, text := range texts {
words := strings.Fields(text)
for _, w := range words {
wordChan <- strings.ToLower(w)
}
}
close(wordChan)
// Wait for all workers to finish
wg.Wait()
return counts
}
Mutexes (sync.Mutex) are essential for thread-safe data structures. For highly concurrent reads/writes on maps, sync.Map provides optimized performance.
SECTION 7 — Practice Tips
Mastering DSA requires structured, consistent practice.
- Recommended Progression: Arrays → Strings → Linked Lists → Stacks/Queues → Trees → Graphs → Dynamic Programming. Don't rush; build a solid foundation.
- Where to Practice: LeetCode perfectly supports Go. Codeforces and AtCoder are excellent for pushing your limits.
- Go-Specific Tooling: Always validate the performance of your implementations. Use
go test -benchto benchmark different approaches andpprofto profile CPU and memory usage of complex algorithms. - Reading List: While "Introduction to Algorithms" (CLRS) provides theory, studying the source code of Go's standard library (e.g.,
sort.go) offers incredible insights into production-grade algorithm implementation.
Go's strict yet simple syntax means you spend less time fighting language idiosyncrasies and more time thinking deeply about the problem at hand. Happy coding!
Let's Connect on:
