I passed a slice to a helper, changed an element, and watched the caller’s data change. Then I appended one more element and watched the helper apparently wander off with its own copy. The language rules explain both results, but the runtime representation makes them difficult to forget.

Start with the smallest example:

func alter(s []int) {
	s[0] = 9
	s = append(s, 4)
}

func main() {
	a := []int{1, 2, 3}
	alter(a)
	fmt.Println(a)
}

A slice value is a small descriptor: a pointer to an underlying array, a length, and a capacity. Passing the slice copies that descriptor, not all the elements. Both descriptors initially point into the same array, so assigning s[0] changes an element visible through a.

append is conditional. If the existing array has enough unused capacity, it can place new elements there and return a descriptor with a larger length. If capacity is exhausted, it allocates another array, copies elements, and returns a descriptor pointing to the new storage. Assigning that returned descriptor only to local s does not update the caller’s descriptor. This is why useful functions return the result of append.

My surprise was that a slice is neither an array nor a conventional pointer to a container object. It is a value describing a window onto array storage. Two slices can overlap, have different lengths, and still share elements. Capacity records how far that particular window may grow before another allocation is needed.

Interfaces add a different descriptor. Conceptually, an empty interface carries a dynamic type and a data word. A non-empty interface also needs information connecting the dynamic concrete type to the interface’s required methods. In the Go 1 runtime this is represented with internal tables and data pointers, but those names and layouts are implementation details, not structures application code may safely reproduce.

The crucial rule is that an interface value has both dynamic type and dynamic value. Assigning a typed nil pointer to an interface records the pointer’s concrete type even though the pointer value is nil:

var p *bytes.Buffer
var x interface{} = p
fmt.Println(x == nil) // false

The interface itself is not nil because its type component is populated. Calls and type assertions use that dynamic type information. This also explains why putting a value into an interface is not the same operation as erasing every fact about it.

Maps look simplest in source and hide the most machinery. A map value refers to a runtime-managed hash table. The runtime hashes a key, selects storage containing possible entries, and compares candidate keys. As entries accumulate, it allocates and organizes more bucket storage. The exact bucket layout, growth thresholds, and hash details belong to this Go implementation and can change without violating Go 1 compatibility.

Copying a map value therefore does not copy every entry. Both values refer to the same underlying map data:

a := map[string]int{"one": 1}
b := a
b["two"] = 2
fmt.Println(a["two"])

That prints 2. A nil map can be read and ranged over, but insertion requires an initialized map because there is no table to receive the entry. make creates the runtime map structure; it does not pre-create a fixed array with one slot per possible key.

Map iteration order is deliberately unspecified. The hash representation is designed for lookup, not for presenting keys in a stable narrative. If output order matters, I collect keys, sort them, and iterate over the sorted slice. Depending on the order observed in one run turns an internal accident into an external protocol.

All three types are cheap to pass as values compared with copying their referenced contents, but “cheap” is not “free” and “references storage” is not “is a reference variable.” The descriptor itself is copied. Reassigning a local slice or map variable does not reassign the caller’s variable. Mutating shared underlying storage may still be visible.

I prefer using these semantic rules in ordinary code and keeping the word-level model for debugging and performance questions. The model explains aliasing, nil interfaces, append, and map copying. The caveat is that runtime source is not the language specification. Code using unsafe to forge these headers ties itself to one compiler version, architecture, and a generous interpretation of luck.

My helper now returns the appended slice, and its caller assigns the result. No reflection and no unsafe arithmetic were required. Internals did their best work by making the simple rule memorable, then staying internal.