notexOPENOverview
Notex/Languages/The Difference Between Maps and Slices
04.10LESSON 10 OF 20 · 4 MIN READ

The Difference Between Maps and Slices

Always free to read

As you saw in this topic, any modifications made to a map that’s passed to a function will be reflected in the original passed-in variable. Now that you know about pointers, you can understand why: within the Go runtime, a map is implemented as a pointer to a struct. Passing a map to a function means that you are copying a pointer.

Because of this, consider carefully before you use maps for input parameters or return values, especially on public APIs. On an API-design level, maps are a bad choice because they say nothing about the values they contain; nothing explicitly defines any keys in the map, so the only way to know what they are is to trace through the code. From the standpoint of immutability, maps are bad because the only way to know what ended up in the map is to trace through all the functions that interact with it. This prevents your API from being self-documenting. If you’re used to dynamic languages, don’t use a map to substitute for another language’s lack of structure. Go is a strongly typed language; rather than passing a map around, use a struct. (You’ll learn another reason to prefer structs when I talk about memory layout in “Reducing the Garbage Collector’s Workload”.)

Note

A map input parameter or return value can be the correct choice in certain situations. Using a struct requires you to name its fields at compile time. If you don’t know the keys for your data at compile time, a map is ideal.

Meanwhile, passing a slice to a function causes more complicated behavior. Any modification to the slice’s contents will be reflected in the original variable. However, if you use append to change the slice’s length, that won’t be reflected in the original variable, even if the slice’s capacity is greater than its length. That’s because a slice is implemented as a struct with three fields: an int field for length, an int field for capacity, and a pointer to a block of memory. Figure 5-8 demonstrates the relationship.

The memory layout of a slice

Figure 5-8. The memory layout of a slice

When you copy a slice to a different variable or pass it to a function, Go makes a copy of the length, capacity, and pointer. Figure 5-9 shows how both slice variables point to the same memory.

The memory layout of a slice and its copy

Figure 5-9. The memory layout of a slice and its copy

Changing the values in the slice changes the memory to which the pointer points, so the changes can be seen in both the copy and the original. You can see in Figure 5-10 how this looks in memory.

Modifying the Contents of a Slice

Figure 5-10. Modifying the contents of a slice

If you append to the slice copy and the original slice has enough capacity for the new values, its length changes in the copy, and the new values are stored in the block of memory that’s shared by the copy and the original. However, the length of the original slice remains unchanged. The Go runtime prevents the original slice from seeing those values, since they are beyond the length of the original slice. Figure 5-11 highlights the values that are visible in one slice variable but not in the other.

Changing The Length Is Invisible In The Original

Figure 5-11. Changing the length is invisible in the original

If you append to the slice copy and there isn’t enough capacity in the original slice for the new values, a new, bigger block of memory is allocated, the values are copied over, and the pointer, length, and capacity fields in the copy are updated. Changes to the pointer, length, and capacity are not reflected in the original; they are only in the copy. Figure 5-12 shows how each slice variable now points to a different memory block.

Changing The Capacity Changes the Storage

Figure 5-12. Changing the capacity changes the storage

The result is that the contents of a slice that’s passed to a function can be modified, but the slice itself can’t be resized. As the only usable linear data structure, slices are frequently passed around in Go programs. By default, you should assume that a slice is not modified by a function. Your function’s documentation should specify whether it modifies the slice’s contents.

Note

The reason you can pass a slice of any size to a function is that the same data type will be passed to the function for any size slice: a struct of two int values and a pointer. The reason you can’t write a function that takes an array of any size is that the entire array would be passed to the function, not just a pointer to the data.

There is one situation where the ability to modify the contents (but not the size) of a slice input parameter is very useful: reusable buffers.

Progress is saved on this device.
KEEP EXPLORING

One note leads to another.

Back to the library →
⌕
Search across the entire library ⌘ K