Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Arrays, Slices, and Maps
Go provides flexible composite types for grouping data.Arrays
An array is a fixed-size sequence of elements of a specific type.[5]int and [10]int are different types entirely. This limits their use cases, but they form the foundation for slices. In practice, you will rarely use arrays directly — think of them as the fixed-size storage that slices sit on top of.
Slices
Slices are dynamically-sized, flexible views into the elements of an array. In practice, slices are much more common than arrays.Slice Internals
A slice does not store any data itself. It just describes a section of an underlying array. Understanding this is crucial for writing efficient Go code.- Pointer: Points to the first element of the slice in the underlying array.
- Length: The number of elements currently in the slice (accessible via
len()). - Capacity: The total number of elements in the underlying array from the slice’s starting point (accessible via
cap()).
Creating Slices
You can create slices usingmake, which allocates a new underlying array:
Appending to Slices
The built-inappend function adds elements to a slice. Here’s what happens under the hood:
- If
len < cap: The element is added to the existing array, and length is incremented. - If
len == cap: A new, larger array is allocated (typically 2x the current capacity), existing elements are copied, and the new element is added.
append returns a new slice value. You must assign it back:
Range
Therange form of the for loop iterates over a slice or map.
_.
Maps
A map maps keys to values. The zero value of a map isnil.
Map Literals
Mutating Maps
Interview Deep-Dive
Explain Go slice internals. What are the pointer, length, and capacity, and how does append decide when to allocate a new backing array?
Explain Go slice internals. What are the pointer, length, and capacity, and how does append decide when to allocate a new backing array?
Strong Answer:
- A slice header is a 24-byte struct (on 64-bit systems) containing three fields: a pointer to the first element in the underlying array, an
intfor length (number of elements in the slice), and anintfor capacity (total elements available from the pointer to the end of the underlying array). - When you call
append, it checks iflen < cap. If yes, it writes the new element into the existing backing array at positionlenand incrementslen. Iflen == cap, it allocates a new, larger array, copies existing elements, adds the new element, and returns a slice header pointing to the new array. The old array becomes eligible for garbage collection if nothing else references it. - The growth strategy is not simply “double every time.” For slices smaller than 256 elements, capacity roughly doubles. For larger slices, it grows by about 25% plus some additional headroom. The exact formula changed in Go 1.18 to produce smoother growth curves and reduce memory waste for large slices.
- The critical implication:
appendmay or may not return a new backing array. This is why you must always assign the result:s = append(s, x). If you forget, you may be working with a stale slice header that points to an old array.
copy to create a deep copy of the data. A defensive pattern is newSlice := append([]T(nil), originalSlice...) which always creates a fresh backing array.You are processing a 100MB byte slice and need to return only the first 3 bytes. A junior developer writes `return data[:3]`. What is wrong with this, and how do you fix it?
You are processing a 100MB byte slice and need to return only the first 3 bytes. A junior developer writes `return data[:3]`. What is wrong with this, and how do you fix it?
Strong Answer:
- The sub-slice
data[:3]creates a new slice header that still points to the original 100MB backing array. As long as this 3-byte slice is reachable, the garbage collector cannot free the original 100MB. This is a classic Go memory leak that does not show up in normal testing because the memory is still technically “in use” — it is just vastly more than you need. - The fix is to copy the data to a new, small slice:
result := make([]byte, 3); copy(result, data[:3]); return result. Now the returned slice has its own 3-byte backing array, and the original 100MB can be garbage collected when nothing else references it. - This pattern shows up frequently in production when reading large files or network payloads and extracting small headers, tokens, or identifiers. I have seen services leak hundreds of megabytes because of this exact pattern in log processing pipelines.
- An alternative using
append:return append([]byte(nil), data[:3]...)also creates a fresh copy. Some developers prefer this for its brevity, thoughcopyis more explicit about the intent.
keys := make([]string, 0, len(m)); for k := range m { keys = append(keys, k) }; sort.Strings(keys). For ordered map semantics, use a slice of key-value pairs or a third-party ordered map implementation.A nil map and an empty map behave differently. Explain the distinction, why this matters for JSON serialization, and how you would handle it in a REST API.
A nil map and an empty map behave differently. Explain the distinction, why this matters for JSON serialization, and how you would handle it in a REST API.
Strong Answer:
- A nil map (
var m map[string]int) and an empty map (m := map[string]int{}orm := make(map[string]int)) both have length 0 and both return zero values when you read from them. But writing to a nil map causes a runtime panic, while writing to an empty map works fine. Under the hood, a nil map has no allocated hash table structure, while an empty map has an initialized but empty hash table. - For JSON serialization, this distinction matters:
json.Marshalencodes a nil map asnulland an empty map as{}. In a REST API, these have different semantic meanings. A"tags": nullmight mean “tags not provided” while"tags": {}means “explicitly no tags.” If your API contract requires an empty object rather than null, you must initialize your maps. - The idiomatic approach for API response structs is to always initialize maps in constructors or use the
omitemptyJSON tag. If the field is optional, useomitemptyso it is omitted entirely when nil. If the field must always be present, initialize it in the constructor:return &Response{Tags: make(map[string]string)}. - This nil-vs-empty distinction also affects equality checks. Two nil maps are equal to each other, and two empty maps are equal to each other, but you cannot compare maps with
==in Go (except to nil). You needreflect.DeepEqualor a manual comparison loop, or in Go 1.21+,maps.Equalfrom the standard library.