3. Data Structures

Slices, maps, and strings: how to size, copy, and use them correctly and safely.

Question: What is the difference between an array and a slice in Go?

Answer: An array has a fixed size determined at compile time and is a value type. A slice is a dynamically-sized, flexible view into the elements of an array. A slice is a small descriptor containing a pointer to the underlying array, a length, and a capacity.

Explanation: Because slices are small descriptors, they are cheap to pass to functions. Arrays, being value types, are copied entirely when passed, which can be expensive. Slices are far more common in idiomatic Go code due to their flexibility. The built-in append function is used to grow slices, which may involve allocating a new, larger underlying array if the capacity is exceeded.

// Array: fixed size of 3
var arr [3]int 

// Slice: dynamic size, backed by an array
var slc []int 

// Create a slice with length 5 and capacity 10
slc = make([]int, 5, 10)

Question: How does append grow slices and how should you preallocate?

Answer: Capacity typically grows by doubling (implementation-dependent). Preallocate with make([]T, 0, n) when you know or can estimate size to avoid reallocations and copying.

Explanation: When capacity is exceeded, a new array is allocated and elements are copied. Keep the returned slice from append.

Question: How do you copy slices and what does copy return?

Answer: Use the built-in copy(dst, src); it returns the number of elements copied (min of lengths). Overlapping copies are allowed for slices.

dst := make([]int, len(src))
n := copy(dst, src)
_ = n

Question: How do you efficiently build strings?

Answer: Use strings.Builder (or bytes.Buffer when you need bytes). Avoid repeated += concatenation in loops.

Explanation: Builder minimizes allocations; call Grow if you can estimate size.

var b strings.Builder
b.Grow(128)
for _, s := range parts { b.WriteString(s) }
result := b.String()

Question: Are maps in Go safe for concurrent use?

Answer: No, the built-in map type is not safe for concurrent reads and writes. If one goroutine is writing to a map while another is reading or writing, a data race will occur, which can lead to a panic or unpredictable behavior.

Explanation: To use maps in a concurrent setting, you must provide your own synchronization. Concurrent unsynchronized access can trigger a runtime panic like fatal error: concurrent map read and map write. Use a sync.Mutex or sync.RWMutex to protect the map, or use the specialized sync.Map type, which is optimized for read-heavy workloads with occasional writes.

Question: Is map iteration order deterministic in Go?

Answer: No. Iteration order over a map is randomized and should not be relied upon.

Explanation: If you need a deterministic order, collect and sort the keys, then range over the sorted keys to access the map values.

Question: What is the difference between a rune and a byte? How does this relate to strings?

Answer: A byte is an alias for uint8 and represents a single byte. A rune is an alias for int32 and represents a single Unicode code point. A Go string is an immutable sequence of bytes, typically encoded in UTF-8.

Explanation: Because UTF-8 is a variable-width encoding, a single character (rune) can consist of one to four bytes. len(s) on a string gives you the number of bytes, not characters. To iterate over the characters (runes) of a string correctly, you must use a for ... range loop, which decodes each UTF-8 sequence.

s := "héllo"
fmt.Println(len(s)) // 6 (bytes)
fmt.Println(utf8.RuneCountInString(s)) // 5 (runes/characters)

for i, r := range s { // r is a rune
    fmt.Printf("index %d, rune %c\n", i, r)
}

Question: How do you test for a key's presence in a map and delete a key safely?

Answer: Use the comma-ok idiom: v, ok := m[k]. Delete with delete(m, k); it’s safe even if the key is absent.

Explanation: ok distinguishes an absent key from a present key with a zero value. delete is idempotent.

Question: How do you cap a resliced slice to prevent overwriting the backing array on append?

Answer: Use a full slice expression s[a:b:c] to set capacity to c-b.

Explanation: Limiting capacity forces append to allocate a new array rather than clobbering data beyond b.