The most important thing to know is that Go slices are
exactly how you would implement a dynamically growable slice in C if I asked you to. No less, but importantly, no more: a pointer to a piece of memory, and a current length and capacity.
The classic slice mistake is to forget that append doesn't necessarily return a completely new slice backed by a disjoint piece of memory, especially easy if you've been using a language where variables are immutable by default.
The problem is that it will often accidentally work; if you over-run the backing store, you will get a new piece of memory:
a := make([]int, 1, 1) // a is backed by a one-element-long piece of memory
a[0] = 42
b := append(a, 17) // this must allocate a new piece of memory
c := append(a, 37) // this must also allocate a new piece of memory
// At this point a == [42], b == [42, 17], c == [42, 37]
The problem is when the existing capacity is sufficient:
a := make([]int, 1, 10) // a is backed by a ten-element-long piece of memory
a[0] = 42
b := append(a, 17) // no need to allocate: just use the existing memory
c := append(a, 37) // no need to allocate: just (re)use the existing memory
// At this point a == [42], b == [42, 37], c == [42, 37]
// ^^^^^^^^^^^^^
The classic slice mistake is to forget that append doesn't necessarily return a completely new slice backed by a disjoint piece of memory, especially easy if you've been using a language where variables are immutable by default.
The problem is that it will often accidentally work; if you over-run the backing store, you will get a new piece of memory:
The problem is when the existing capacity is sufficient: