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.
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.
Go is a statically typed language with type inference using :=. For DSA, a small set of types covers most problems.
| Type | Size | Range | DSA Use Case |
|---|---|---|---|
int | platform-dependent (almost always 8 bytes on 64-bit) | -9.2 x 10^18 to 9.2 x 10^18 | Default integer for everything |
int32 | 4 bytes | -2.1B to 2.1B | When you need exactly 32 bits |
int64 | 8 bytes | -9.2 x 10^18 to 9.2 x 10^18 | Explicit 64-bit math |
byte (alias for uint8) | 1 byte | 0 to 255 | ASCII characters, raw bytes |
rune (alias for int32) | 4 bytes | -2.1B to 2.1B | Unicode code points |
bool | 1 byte | true / false | Visited arrays, flags |
float64 | 8 bytes | ±1.7 x 10^308 | Rarely 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:
int32 value).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.
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:
| Operator | Meaning | Example |
|---|---|---|
& | AND | n & 1 checks if n is odd |
| | OR | mask | (1 << i) sets bit i |
^ | XOR | a ^ b flips differing bits |
&^ | AND NOT (bit clear) | mask &^ (1 << i) clears bit i |
<< | Left shift | 1 << k equals 2^k |
>> | Right shift | n >> 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:
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:
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:
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.
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 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.
append always returns a new slice header. You must assign the result back. Forgetting this is a classic bug:
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.
When initializing a 2D slice, do not try to share a single inner slice across rows:
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.
| Operation | Code | Time | Notes |
|---|---|---|---|
| Length | len(nums) | O(1) | |
| Append | nums = append(nums, x) | O(1) amortized | Returns new header |
| Pop last | nums[len(nums)-1]; nums = nums[:len(nums)-1] | O(1) | Stack pattern |
| Reverse in place | for i, j := 0, len(nums)-1; i < j; i, j = i+1, j-1 { nums[i], nums[j] = nums[j], nums[i] } | O(n) | |
| Clone | append([]int(nil), nums...) | O(n) | Independent backing array |
| Contains | linear scan | O(n) | No built-in helper |
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.
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.
| Operation | Code | Returns | Notes |
|---|---|---|---|
| Length | len(s) | int | Byte length |
| Character at | s[i] | byte | Use []rune(s)[i] for Unicode |
| Substring | s[i:j] | string | Half-open, O(1) (shares backing memory) |
| Concatenation | s + t | string | O(n + m), allocates |
| Contains | strings.Contains(s, "ab") | bool | |
| Index of | strings.Index(s, "ab") | int | -1 if not found |
| Split | strings.Split(s, " ") | []string | Tokenize |
| Join | strings.Join(parts, "-") | string | |
| Replace | strings.ReplaceAll(s, "a", "b") | string | |
| Trim | strings.TrimSpace(s) | string | |
| Lowercase | strings.ToLower(s) | string | |
| Uppercase | strings.ToUpper(s) | string | |
| Repeat | strings.Repeat("ab", 3) | string | "ababab" |
| Prefix check | strings.HasPrefix(s, "ab") | bool | |
| Suffix check | strings.HasSuffix(s, "ab") | bool |
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:
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).
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.
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.
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.
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.
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:
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.
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.
There is no Collections.reverseOrder() equivalent. Two idioms cover almost every case:
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.
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.
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.
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:
Modifying a slice or map while iterating over it is a source of subtle bugs. The rules for each container differ.
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".
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 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:
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.
Go has no built-in graph type, so you build representations from slices and maps.
Best when node IDs are dense integers from 0 to n-1:
Best when node IDs are arbitrary (strings, sparse integers):
For Dijkstra and shortest-path problems, store the weight alongside the neighbor:
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.
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.
Removing duplicates is a common DSA subtask. Three patterns cover most cases.
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.
Best when input order is preserved or sorting is undesirable:
Slices cannot be set keys. Convert each slice to a comparable form (an array or a string) first:
A short reference of small patterns and utility calls that appear repeatedly across problems.
math.Abs only works on float64. For integers, write your own:
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.
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.
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.
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.