The Friction is the Feature
You paste a Python solution into a Go editor. The red squiggles appear immediately. nums is a slice, not a list. len is a function, not a method. map needs make. You feel like you are translating syntax rather than solving the problem. That friction is real. Go does not hide the mechanics. Every allocation, every type, every pointer is explicit. LeetCode in Go forces you to learn these mechanics. The payoff is code that runs fast and behaves predictably. You stop guessing how the runtime works and start knowing.
Think of Go as a workshop with labeled tools. Python is a magic box where you shout a command and something happens. In Go, you pick the wrench, you turn the bolt, you hear the click. When you are solving algorithms, that click matters. You know exactly where the data lives. You know when a copy happens. You know when a pointer is nil. This clarity prevents subtle bugs that hide in dynamic languages until production.
LeetCode problems are single-threaded and focused on algorithmic logic. Real Go code deals with concurrency, errors, and contexts. The patterns you learn here apply to both. You will use slices, maps, and pointers in production just as you do in competitive programming. The difference is that production code also handles errors and respects cancellation. LeetCode is a safe place to master the data structures and idioms before adding the plumbing.
Slices and Maps: Memory and Lookup
Slices and maps are the workhorses of Go algorithms. A slice is a descriptor pointing to an underlying array. It holds a pointer, a length, and a capacity. When you pass a slice to a function, you pass the descriptor by value. The function sees the same underlying array. This means modifications inside the function affect the caller. This is efficient. It avoids copying large arrays. It also means you have to be careful. If you append to a slice inside a function and the capacity is exceeded, a new array is allocated, and the caller does not see the change.
Maps are hash tables. They are reference types. Iteration order is randomized. This is a feature, not a bug. It prevents your code from depending on insertion order. If you need order, collect keys and sort them. Maps do not guarantee order, and relying on order is a bug waiting to happen.
Here is the classic Two Sum problem rewritten with Go idioms. The solution uses a map for O(1) lookups and the ok idiom to check existence.
// TwoSum returns indices of two numbers that add up to target.
func TwoSum(nums []int, target int) []int {
// Map stores value -> index. Zero value for int is 0, so we need the "ok" check.
seen := make(map[int]int)
for i, num := range nums {
complement := target - num
// Check existence explicitly. Accessing a missing key returns 0, which is ambiguous.
if j, ok := seen[complement]; ok {
return []int{j, i}
}
seen[num] = i
}
// Return nil slice if no solution found.
return nil
}
The ok idiom is essential. If you access a map key that does not exist, Go returns the zero value for the value type. For integers, that is 0. If index 0 is a valid answer, you cannot distinguish between a missing key and a key with value 0. The ok boolean tells you whether the key exists. This pattern appears everywhere in Go. It is the map equivalent of error checking.
Convention aside: In real Go code, exported functions start with a capital letter. LeetCode often expects lowercase function names to match the problem signature. In your own projects, follow the convention. Public names start with a capital. Private names start with a lowercase. There are no public or private keywords. Capitalization is the rule.
Maps are fast. The ok idiom is your safety net.
Pointers and Structs: Building Graphs
Go has pointers. They are simple. A *T holds the address of a T. You use . to access fields, not ->. This is a deliberate design choice. It keeps the syntax clean. Pointers are essential for linked lists, trees, and graph nodes. They are also essential for passing large structs to functions without copying. If you pass a struct by value, Go copies the entire struct. If you pass a pointer, Go copies the address. The copy is cheap. The mutation is visible.
Here is a linked list reversal. The function takes a pointer to the head node and returns a pointer to the new head.
// ListNode defines a node in a singly linked list.
type ListNode struct {
Val int
Next *ListNode
}
// ReverseList reverses a linked list in place.
func ReverseList(head *ListNode) *ListNode {
var prev *ListNode
// Iterate until head is nil.
for head != nil {
// Save the next node before overwriting the pointer.
next := head.Next
// Point current node back to the previous one.
head.Next = prev
// Advance prev and head.
prev = head
head = next
}
return prev
}
Pointers can be nil. Dereferencing a nil pointer causes a runtime panic. The compiler cannot catch all nil dereferences. You must check pointers before using them. If a function returns a pointer, it might return nil. If a struct field is a pointer, it might be nil. Always verify.
Convention aside: Receiver names in Go are usually one or two letters matching the type. (b *Buffer) Write(...) is standard. (this *Buffer) or (self *Buffer) are not idiomatic. LeetCode solutions often skip methods, but when you define methods on structs, use short receiver names.
Pointers are explicit. If it is a pointer, it can be nil. Check it.
Sorting and Searching: The Standard Library
The sort package is powerful. sort.Strings and sort.Ints are convenience functions. For custom sorting, you implement sort.Interface. This requires three methods: Len, Less, and Swap. It feels verbose compared to a lambda. The verbosity is the price of performance and type safety. You define the comparison logic explicitly. The compiler checks the types. The sort algorithm is optimized for the interface.
Here is a custom sort for strings by length. The type ByLength implements sort.Interface.
import "sort"
// ByLength implements sort.Interface for []string based on length.
type ByLength []string
func (a ByLength) Len() int { return len(a) }
func (a ByLength) Less(i, j int) bool { return len(a[i]) < len(a[j]) }
func (a ByLength) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
// SortByLength sorts a slice of strings by length in place.
func SortByLength(words []string) {
// Sort uses a fast introsort algorithm.
sort.Sort(ByLength(words))
}
Binary search is available via sort.Search. The function returns the insertion point, not a boolean. You must verify the result. If the index is within bounds and the value matches, the target is found. Otherwise, it is not.
// BinarySearch finds the index of target in a sorted slice.
func BinarySearch(nums []int, target int) int {
// SearchInts returns the index if found, or the insertion point.
idx := sort.SearchInts(nums, target)
// Verify the index is within bounds and matches the target.
if idx < len(nums) && nums[idx] == target {
return idx
}
return -1
}
sort.Search returns an index, not a boolean. Always verify the result.
Pitfalls: Capacity, Nil, and Order
Slice capacity is a common source of bugs. A slice has a length and a capacity. The length is the number of elements. The capacity is the size of the underlying array. When you append to a slice, Go checks if there is room. If the length equals the capacity, Go allocates a new array, copies the data, and updates the slice. This means the original slice and the new slice no longer share the underlying array.
If you sub-slice a slice, the new slice shares the underlying array. If you append to the sub-slice, you might overwrite data in the original slice if the capacity allows. This is dangerous. Use append carefully. Or use make to create a new slice with independent storage.
Map iteration order is randomized. Never assume order. If you need deterministic output, collect keys and sort them.
Nil vs empty slices matter. var s []int is nil. s := []int{} is empty. They behave the same for most operations. They differ when marshaling to JSON. A nil slice becomes null. An empty slice becomes []. LeetCode usually does not care, but real APIs do.
The compiler catches many mistakes. If you forget to capture a loop variable, the compiler rejects the program with loop variable i captured by func literal (which became a hard error in Go 1.22+). If you pass the wrong type, you get cannot use x (untyped int constant) as string value in argument. If you import a package and do not use it, you get imported and not used. Read the errors. They tell you exactly what is wrong.
Convention aside: gofmt is the standard formatter. It removes arguments about indentation and style. Most editors run it on save. LeetCode solutions often ignore gofmt because of the function signature constraints, but in real code, run gofmt. Trust the tool.
Slices share underlying arrays. Be careful with sub-slices.
Decision Matrix
Use a slice when you need ordered data and fast indexing. Use a map when you need O(1) lookups by key. Use a pointer when you need to mutate a struct or represent a null state. Use sort.Search when you need binary search on a sorted slice. Use container/heap when you need a priority queue, but consider a third-party library if the interface boilerplate feels excessive. Use plain sequential code when you do not need concurrency: the simplest thing that works is usually the right thing.
Do not pass a *string. Strings are already cheap to pass by value. Do not fight the type system. Wrap the value or change the design.