AlgoMaster Logo

Go Crash Course for DSA

51 min readUpdated May 29, 2026
Listen to this chapter
Unlock Audio

This chapter is a focused crash course on the Go features that come up repeatedly in DSA and coding interview preparation. Instead of covering the entire language, we will concentrate only on the parts that matter for solving problems efficiently in interviews.

Common Imports

On LeetCode, your package main is set up for you and most standard library packages are available. You only need to import what your solution uses. Here are the packages that cover almost every DSA problem:

Most DSA solutions need only three or four packages. Unused imports are compile errors in Go, so remove anything you do not use. Goimports and gofmt handle this automatically in a real editor, but on LeetCode you have to manage it by hand.

Variables, Types, and the Overflow Trap

Go is a statically typed language with type inference using :=. For DSA, a small set of types covers most problems.

TypeSizeRangeDSA Use Case
intplatform-dependent (almost always 8 bytes on 64-bit)-9.2 x 10^18 to 9.2 x 10^18Default integer for everything
int324 bytes-2.1B to 2.1BWhen you need exactly 32 bits
int648 bytes-9.2 x 10^18 to 9.2 x 10^18Explicit 64-bit math
byte (alias for uint8)1 byte0 to 255ASCII characters, raw bytes
rune (alias for int32)4 bytes-2.1B to 2.1BUnicode code points
bool1 bytetrue / falseVisited arrays, flags
float648 bytes±1.7 x 10^308Rarely used, some geometry problems

Go's int is platform-dependent, but on every machine that runs LeetCode it is 64-bit. This makes overflow far less common than in many other languages because most DSA problems stay well under 2^63 - 1. Even so, two cases still need care:

  1. Explicit 32-bit math when a problem specifies a 32-bit result (e.g., LeetCode's "Reverse Integer" returns an int32 value).
  2. Multiplication that could exceed 2^63 (e.g., two large int64 values multiplied without modulo).

The short declaration := only works inside functions. Package-level variables must use var.

Constants and iota are useful for direction arrays and enum-like values:

Min and Max. Go 1.21 added built-in min and max functions that work on any ordered type. For older Go versions or when you want explicit float handling, use math.Min and math.Max (which only work on float64).

The math package also gives you sentinel constants for "infinity" patterns:

Go has no ternary operator. The idiomatic replacement is an if-else block, or min/max for the common "pick the bigger one" case.

Operators and Control Flow

Most operators in Go work as you would expect, but a few deserve special attention for DSA.

Modular arithmetic with % appears in hashing, cyclic array problems, and math-heavy questions. Go's % can return negative values for negative operands: -7 % 3 gives -1, not 2. To get a non-negative result, use ((n % m) + m) % m.

Many problems ask you to return the result "modulo 10^9 + 7" to prevent overflow in the output:

Integer division in Go truncates toward zero, not toward negative infinity. This differs from some other languages, so be careful with negative numerators:

Bitwise operators appear in subset enumeration, bitmask DP, and bit-tricks problems:

OperatorMeaningExample
&ANDn & 1 checks if n is odd
|ORmask | (1 << i) sets bit i
^XORa ^ b flips differing bits
&^AND NOT (bit clear)mask &^ (1 << i) clears bit i
<<Left shift1 << k equals 2^k
>>Right shiftn >> 1 divides by 2

Key bitwise operators:

The &^ (AND NOT) operator is specific to Go. a &^ b is equivalent to a & (^b), which clears the bits in a that are set in b. It is convenient for bit clearing.

Short-circuit evaluation with && and || matters for bounds checking. The right side of the operator is not evaluated if the left side determines the result:

Control Flow

Go has only one loop keyword: for. It serves as while, do-while, and traditional for:

The for-range form copies the value. For large structs this can be expensive. For DSA on primitives like int it is fine.

Map iteration is randomized. Each pass over a map visits keys in a different order. If you need deterministic iteration, sort the keys first.

The switch statement does not fall through by default and supports expression switches:

Functions and Multiple Return Values

Go functions can return multiple values, which is the idiomatic way to signal success or failure without exceptions:

The value, ok pattern is everywhere in Go. Map lookups, type assertions, and channel receives all follow it.

Named return values can act as documentation and let you use a bare return. In DSA they are mostly used for readability:

Variadic functions accept a variable number of arguments:

Pass-by-Value Semantics

Go is always pass-by-value. Primitives, structs, and arrays are copied. However, slices, maps, and channels are reference types internally, so passing them to a function lets the function modify the underlying data:

This distinction matters when you write helper functions for DSA. If you want to mutate an integer or a struct from a helper, take a pointer. If you want to mutate a slice's elements, pass the slice directly.

Closures

Functions are first-class in Go. Closures capture variables from the enclosing scope by reference:

The closure pattern lets a recursive helper share result, nums, and other state without passing them as parameters. This is the standard Go style for backtracking and DFS in DSA.

There is a subtle gotcha when capturing loop variables in older Go versions. Before Go 1.22, the loop variable was shared across iterations:

LeetCode typically runs Go 1.21+, so the modern semantics apply. The i := i shadow is still a safe habit if you are unsure.

Slices: Go's Dynamic Array

Slices are the core dynamic-array type in Go. They look like arrays but behave very differently, and the differences are where most Go bugs in DSA come from.

A slice is a three-word descriptor: a pointer to an underlying array, a length, and a capacity. Multiple slices can point into the same backing array, which is the source of most slice surprises.

Creating Slices

Slice Operations

append always returns a new slice header. You must assign the result back. Forgetting this is a classic bug:

The Aliasing Trap

The key Go gotcha for DSA: slicing does not copy data. The new slice shares the same backing array as the original. Mutating one can change the other.

This is especially dangerous in backtracking, where you build up a path slice and want to save snapshots of it. If you save the slice directly, every saved entry will be a window into the same backing array:

The append([]int(nil), path...) idiom is the shortest way to clone a slice in one expression. The copy plus make version is more explicit.

The 2D Slice Pitfall

When initializing a 2D slice, do not try to share a single inner slice across rows:

Removing Elements

Go has no built-in delete for slices. You concatenate the two sides:

For DSA stacks, growing and shrinking from the end with append and nums[:len(nums)-1] gives amortized O(1) operations. This is the standard way to implement a stack in Go.

Common Slice Operations Reference

OperationCodeTimeNotes
Lengthlen(nums)O(1)
Appendnums = append(nums, x)O(1) amortizedReturns new header
Pop lastnums[len(nums)-1]; nums = nums[:len(nums)-1]O(1)Stack pattern
Reverse in placefor i, j := 0, len(nums)-1; i < j; i, j = i+1, j-1 { nums[i], nums[j] = nums[j], nums[i] }O(n)
Cloneappend([]int(nil), nums...)O(n)Independent backing array
Containslinear scanO(n)No built-in helper

Strings and Runes

Go strings are immutable sequences of bytes. UTF-8 encoded by convention, but the type itself is just bytes. This dual nature, raw bytes underneath and UTF-8 text by convention, is central to Go string problems.

Byte vs Rune

s[i] returns a byte (uint8). This is correct for ASCII strings, which covers most DSA problems. For Unicode, you need runes.

For LeetCode problems, the input is typically ASCII. Use []byte for in-place mutation. Reserve []rune for problems that explicitly mention Unicode.

Common String Methods

OperationCodeReturnsNotes
Lengthlen(s)intByte length
Character ats[i]byteUse []rune(s)[i] for Unicode
Substrings[i:j]stringHalf-open, O(1) (shares backing memory)
Concatenations + tstringO(n + m), allocates
Containsstrings.Contains(s, "ab")bool
Index ofstrings.Index(s, "ab")int-1 if not found
Splitstrings.Split(s, " ")[]stringTokenize
Joinstrings.Join(parts, "-")string
Replacestrings.ReplaceAll(s, "a", "b")string
Trimstrings.TrimSpace(s)string
Lowercasestrings.ToLower(s)string
Uppercasestrings.ToUpper(s)string
Repeatstrings.Repeat("ab", 3)string"ababab"
Prefix checkstrings.HasPrefix(s, "ab")bool
Suffix checkstrings.HasSuffix(s, "ab")bool

Building Strings Efficiently

Concatenating in a loop with + is O(n^2) because each + allocates a new string. Use strings.Builder for O(n) string building:

For ASCII-only problems, working directly with a []byte and converting to string at the end is even simpler and often faster:

Character Arithmetic

Go's byte and rune are integer types, so you can do arithmetic directly:

This is faster and more memory-efficient than a map[byte]int when the character set is limited to lowercase (or uppercase, or digits).

String to Int Conversion

Maps

Go's map is a hash map. It appears in most DSA problems alongside slices, providing O(1) average-case lookups, inserts, and deletes.

The zero-value-on-miss behavior is convenient for counters. freq[key]++ works correctly even when key is new. If you need to distinguish "missing" from "present but zero", use the value, ok := m[key] form.

Frequency Counting

The most common DSA pattern is counting characters or numbers:

The [26]int array is faster than map[byte]int when the character set is known and small. It is also simpler to compare two such arrays directly (count1 == count2), which is useful for anagram checking.

Sets in Go

Go has no built-in set type. The idiomatic substitute is map[T]bool or map[T]struct{}. The struct{} version uses zero bytes per entry, which can matter for very large sets:

For most DSA problems, map[T]bool is fine and reads more naturally. Save map[T]struct{} for problems with tight memory limits.

Composite Keys

Maps require comparable keys. You can use structs as keys when you need to track a multi-dimensional state (e.g., (row, col) in BFS, (index, remaining) in DP):

Slices are not comparable, so you cannot use a []int as a map key. Convert to an array or to a string instead. For fixed-size keys, an array type works:

This is the standard Go idiom for grouping anagrams: the [26]int frequency array becomes the map key.

Map Iteration Is Randomized

Iteration order over a Go map is intentionally randomized to discourage code that depends on order. If you need deterministic iteration, collect the keys, sort them, then iterate:

Sorting and Comparators

The sort package covers the common DSA needs. Most sorting is done with the convenience helpers sort.Ints, sort.Strings, and the generic sort.Slice.

Custom Comparators

For anything more complex, use sort.Slice with a less function:

The less function returns true when element i should come before element j. Given that rule, multi-key sorts become a matter of chaining returns.

sort.SliceStable preserves the relative order of equal elements. Use it when ties need to keep input order:

sort.Slice uses introsort with a pattern-defeating optimization and is not guaranteed stable. Use the Stable variant whenever stability matters.

sort.Search does a generic binary search over an index range. The trick is that you provide a predicate that is false for low indices and true for high indices, and it returns the first true index:

If the target is missing, sort.Search returns len(nums). Always check the returned index against the length before accessing the slice.

Reverse Sort

There is no Collections.reverseOrder() equivalent. Two idioms cover almost every case:

The Heap Interface

Go does not have a built-in priority queue. The container/heap package provides the algorithm; you write the storage and the comparator.

Implementing a heap takes five methods: Len, Less, Swap, Push, and Pop. The first three come from sort.Interface. The last two grow and shrink the underlying slice.

Use the heap through the heap.Push and heap.Pop functions (note the package-level functions, not the methods you wrote):

The heap.Push and heap.Pop calls handle the sift-up and sift-down. Your Push and Pop methods just add or remove the last element. Mixing them up is a common bug; always go through the package functions to maintain the heap invariant.

Max-Heap

To make a max-heap, flip the comparator:

Or, for a quick fix, push negative values into a min-heap and negate them when popping.

Heap of Pairs

For Dijkstra and priority-queue patterns, you usually need to push a (priority, payload) pair:

Writing the heap once and copying the boilerplate per problem is the standard Go workflow. Some people keep a templated implementation handy for interviews.

Nil Handling

Go has no exceptions for null access; instead, dereferencing a nil pointer triggers a runtime panic. In DSA, nil shows up in tree traversal, linked list traversal, and map lookups.

A nil slice behaves like an empty slice for most read operations. len, range, and append all handle it gracefully. The same is not true for maps: a nil map can be read from (returns zero values), but writing to it panics.

This bites when you build a map[K]V by walking input and lazily creating sub-maps:

Iterating and Modifying Collections Safely

Modifying a slice or map while iterating over it is a source of subtle bugs. The rules for each container differ.

Slices

You can mutate elements of a slice during a range loop, but be careful: range copies the value. Modifying the copy does not modify the slice:

Appending to the slice while ranging is well-defined (the loop reads the original length captured at the start), but it usually creates confusing code. Build a new slice instead.

For in-place filtering, the standard idiom uses a write pointer:

This is the Go version of the classic two-pointer filter from "Remove Element" and "Remove Duplicates from Sorted Array".

Maps

You can delete entries from a map during iteration. It is safe and well-defined: the entry will not be visited again. You cannot rely on whether a newly added entry will be visited; the spec leaves this implementation-defined.

For most DSA problems, building a new map or slice is clearer than mutating one during iteration.

Recursion and the Call Stack

Recursion is the natural fit for tree traversal, DFS, backtracking, and divide-and-conquer. Go's default goroutine stack starts small (around 8 KB) and grows on demand, so deep recursion is supported, but stack overflows can still happen for very deep recursion on adversarial input.

Go does not optimize tail recursion. A recursive function that does its work in the tail position still consumes a stack frame per call. For problems with potentially very deep recursion (a skewed tree of 10^5 nodes, for example), convert to an iterative solution with an explicit stack:

Backtracking Template

The pattern for backtracking in Go uses a closure to share state with a recursive helper:

The cp := make([]int, len(path)); copy(cp, path) pattern is essential. If you push path directly into result, every result entry will alias the same backing array, and they will all change as the backtrack unwinds. This is the Go equivalent of the classic backtracking copy bug.

Graph Representation Patterns

Go has no built-in graph type, so you build representations from slices and maps.

Adjacency List with a Slice

Best when node IDs are dense integers from 0 to n-1:

Adjacency List with a Map

Best when node IDs are arbitrary (strings, sparse integers):

Weighted Edges

For Dijkstra and shortest-path problems, store the weight alongside the neighbor:

BFS Template

The queue = queue[1:] pop is O(1) for the slice header update, but the unused element still occupies memory in the underlying array until the next append triggers reallocation. For very large BFS, this can matter; use an index-based head pointer instead:

This avoids the slice-head churn entirely and is faster for large graphs.

Pairs and Tuples

Go has no built-in tuple. The idiomatic replacement is a small struct for clarity, or a [2]int array when you need two ints.

The [2]int array is comparable and hashable, so it can be a map key. A []int slice cannot be a map key because slices are not comparable. Use the array form whenever you need to put a pair into a map or set.

For more than two or three values, a named struct beats anonymous types for readability. Names like Edge, State, and Step document intent better than the third element of an array.

Deduplication Patterns

Removing duplicates is a common DSA subtask. Three patterns cover most cases.

Sort and Skip

Best for problems where the result must be sorted anyway (3Sum, 4Sum):

This runs in O(1) extra space beyond the sort and produces results in sorted order.

Set-Based Deduplication

Best when input order is preserved or sorting is undesirable:

Deduplicating Slices of Slices

Slices cannot be set keys. Convert each slice to a comparable form (an array or a string) first:

Common DSA Idioms in Go

A short reference of small patterns and utility calls that appear repeatedly across problems.

Two Pointers

Sliding Window

Stack with a Slice

Queue with a Slice (and Index Head)

Direction Arrays for Grid Traversal

Counting with a Fixed Array

Abs and Sign

math.Abs only works on float64. For integers, write your own:

Power of Two Check

Ceiling Division for Positive Integers

Struct Basics for Design Problems

DSA problems often define custom node types. These definitions appear throughout the course.

LeetCode provides these definitions for you, so you only need to write your solution function. Fields use capitalized names because Go uses capitalization for visibility, and LeetCode's runners expect the exported form.

Constructing Nodes

Two common ways to construct a struct:

The &T{} form returns a pointer to a new value. This is the standard way to allocate a struct on the heap in Go. The compiler decides whether the value actually lives on the heap or the stack based on escape analysis, but you do not need to think about that for DSA.

Methods

Go has no classes. You attach methods to types using a receiver:

Use a pointer receiver (*Trie) when the method modifies the receiver or when the type is large enough that copying it would be wasteful. For DSA, pointer receivers are almost always the right choice on struct types.

LeetCode Design Problem Skeleton

The Constructor function returns the value, not a pointer. LeetCode's harness handles the pointer for you when it calls methods. Stick to this signature for design problems; deviating from it breaks the test runner.

This crash course covers the Go features that come up most often in DSA problems. The rest of the course assumes you can read and write these patterns fluently. When a later chapter shows a Go solution, the building blocks here are the ones it uses.