liveBook · Manning

Backing array

As explained in 100 Go Mistakes and How to Avoid Them

A backing array is the underlying array that stores the elements referenced by a Go slice.

A Go slice stores its elements in a backing array. The slice holds a pointer to that array, a length, and a capacity. Length limits which elements can be accessed; capacity describes the available space in the array. Confusing the two can cause unexpected writes, allocations, or retained memory. section 3.4

Two slices, one array

With `s1 := make([]int, 3, 6)` and `s2 := s1[1:3]`, the slices share a backing array. Changing a common element through either slice is visible through both. An `append` may also write into that shared array while capacity remains; once it is full, Go creates another array and copies the elements. section 3.4

Choose an approach based on the problem:

  • When the result size is known, use `make([]T, 0, n)` before appending, or `make([]T, n)` and assign by index. This avoids repeated backing-array allocations and copies. section 3.5
  • When retaining a small part of a large `[]byte`, copy it into a new slice. Returning `msg[:5]` can keep the entire message array alive; `msg[:5:5]` limits capacity but does not make the remaining storage reclaimable. section 3.10.1
  • When discarding elements containing pointers or slice fields, copy the wanted elements or clear the unwanted references before slicing. Reducing length alone does not release what those elements reference. section 3.10.2
func main() {
    foos := make([]Foo, 1_000)              #1
    printAlloc()
 
    for i := 0; i < len(foos); i++ {        #2
        foos[i] = Foo{
            v: make([]byte, 1024*1024),
        }
    }
    printAlloc()
 
    two := keepFirstTwoElementsOnly(foos)   #3
    runtime.GC()                            #4
    printAlloc()
    runtime.KeepAlive(two)                  #5
}
 
func keepFirstTwoElementsOnly(foos []Foo) []Foo {
    return foos[:2]
}

The two-element result still references the original array of 1,000 structs. After garbage collection, the allocations referenced by the other elements remain retained. section 3.10.2

A slice assignment is not a copy of its elements: both slices still reference the same array. This matters across goroutines. Concurrent `append` calls to a full slice receive separate arrays; calls with spare capacity can write the same backing-array index and race. Copy first when each goroutine needs isolated storage. section 9.10 section 9.9

The retention risk also appears with strings: with the standard Go compiler, storing a short substring of a large log can retain the original backing array. Make a separate copy when the substring must outlive the log. section 5.6

How 100 Go Mistakes and How to Avoid Them covers it

How deep it goes
Traces slicing and append through shared storage, then demonstrates garbage-collection retention and copy-based fixes.
Examples
Uses a five-byte message type retaining a million-byte array and a two-element slice retaining allocations from 1,000 structs.
What it assumes
Assumes familiarity with Go slice operations while defining the slice’s pointer, length, and capacity.
What you can do after
Diagnose/debug backing-array sharing and retention; choose preallocation, copying, or clearing references according to the problem.
How it connects
Connects slice length, capacity, append, garbage collection, string substrings, and data races.

Also explained in

Compare how 3 books explain backing array →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of 100 Go Mistakes and How to Avoid Them. The section links point to the parts of the book it draws on.