Last Updated: May 22, 2026
Slices give you append and indexing, and that's about it. Everything else (deleting an element, inserting in the middle, filtering out items, reversing in place) is built by hand from those primitives, and the standard idioms for doing so live on the Go wiki's "Slice Tricks" page. Learning them once pays off forever: the same handful of patterns shows up in cart code, inventory updates, wishlist management, and order processing. Go 1.21+ added slices.Delete, slices.Insert, and friends, covered in the next chapter; here we build the patterns by hand so you understand what's actually happening to the backing array underneath.
The most common operation is removing a single element from a slice while keeping the other elements in their original order. The idiom uses append with a two-slice spread to glue the prefix before the index to the suffix after it.
The expression cart[:i] is the slice up to but not including index i. The expression cart[i+1:] is everything from i+1 to the end. Joining them with append(cart[:i], cart[i+1:]...) shifts every element after i one slot to the left, on top of the element we wanted to drop, and the result is reassigned to cart. The ... after the second slice spreads its elements into append as individual arguments.
This is O(n - i). Every element after the deleted index moves left by one slot. Deleting from the front of a 10,000-element slice copies 9,999 elements.
If the element type holds pointers or large data, the tail slot still references the old object, which can keep memory alive longer than expected. For now just be aware that for plain values like int, float64, or short strings, it doesn't matter.
When you don't care about order, there's a much faster way to delete an element. Swap the element at index i with the last element, then truncate the slice by one. This is O(1) regardless of where i is in the slice.
products[i] = products[len(products)-1] overwrites the element to delete with the last element. products = products[:len(products)-1] drops the now-duplicated last slot by reslicing to length minus one. The order changes: webcam moved from the end to position 1, and mouse is gone.
O(1). One assignment and one reslice, no matter how long the slice is.
Use this when the slice represents an unordered collection: a set of tags on a product, a pool of available promo codes, a list of active sessions. Use the preserve-order version when the slice is ordered by something meaningful, like the items in a cart shown in the order the user added them.
The diagram shows the two steps in sequence. Step 1 puts the last element in the slot we're vacating, step 2 chops the tail.
To insert a new element at a specific position, the idiom mirrors the preserve-order delete: split the slice at the insertion point, glue the new element between the two halves, and reassign.
The inner append([]string{item}, cart[i:]...) builds a new slice that starts with item and continues with everything from index i onward. The outer append glues that to cart[:i]. The result is the original prefix, the inserted item, and the original suffix, in order.
O(n - i + extra). The suffix shifts right by one, and the inner append allocates a fresh slice. Inserting near the front of a long slice is expensive.
The double-nested append looks awkward, and it is. A clearer (and slightly more efficient) alternative grows the slice by one with a zero value, then shifts the suffix right and writes the new element into the gap.
Both forms produce the same result. The first is shorter and shows up more often in real code; the second avoids the inner allocation when the backing array has spare capacity. Pick whichever reads better in context.
Adding an element to the front of a slice is just insert at index 0, but it comes up enough to call out separately.
append([]string{newest}, recent...) builds a new slice that starts with the new element and continues with every element of the original. The original slice is unchanged; the result is a brand-new slice with a fresh backing array.
O(n) and always allocates. Every prepend copies the entire slice into a new backing array, so building a list by prepending in a loop is O(n^2). If you need a "newest first" view, append to the end and reverse once at the end, or use a different data structure.
If you're tempted to prepend repeatedly to maintain a most-recent-first list, stop and think. Appending is amortized O(1); prepending is always O(n). Build the list with append, then reverse if you need the other order.
To drop every element that fails a predicate, walk the slice with two indices: one to read, one to write. Copy each surviving element into the write position, then truncate.
The variable n is the write index, the count of elements kept so far. The range loop reads each element. When the predicate p <= maxPrice holds, we write that element to prices[n] and advance n. When the predicate fails, we skip the write. After the loop, prices[:n] keeps only the survivors.
O(n) time, O(1) extra space. The slice is reused as both input and output.
This pattern is the standard tool for in-place filtering. It modifies the slice but allocates nothing. The elements past index n still hold the old values until the reslice; for plain numbers this is harmless, but for pointer-bearing types you may want to zero them out before truncating (covered in the gotchas chapter).
When you need to keep the original intact, build a fresh slice and append matching elements to it.
make([]int, 0, len(stock)) allocates a fresh slice with length 0 but capacity equal to the original. Pre-sizing the capacity means append never reallocates inside the loop, even in the worst case where every element passes the filter.
O(n) time, O(k) space where k is the number of survivors. Pre-allocating with make([]T, 0, len(src)) avoids reslices during the loop.
Use this form when the original slice is shared, when you need both the filtered and unfiltered versions, or when you don't want to mutate caller-owned data.
The diagram contrasts the two outputs of this version: the new available slice on one path, and the unchanged stock slice on the other.
To reverse a slice without allocating, swap the first element with the last, the second with the second-to-last, and so on until the indices meet.
Go's parallel assignment makes the swap a one-liner: history[i], history[j] = history[j], history[i]. The loop initializes two indices, advances them toward each other, and stops once they meet or cross. Odd-length slices leave the middle element untouched, which is correct.
O(n / 2) swaps, O(1) extra space. Faster than building a reversed copy with append in a backwards loop.
"Pop" returns the last element and removes it from the slice. The idiom is two lines: read the value, then reslice to drop the last slot.
stack[len(stack)-1] reads the tail. stack[:len(stack)-1] reslices to drop it. Together they implement a stack-style pop. The order is important: read first, then truncate, because the index expression has to land before you shorten the slice.
O(1). No element movement, just a header update.
This is the building block for stack algorithms (depth-first traversal, balanced parentheses, undo histories). Pair it with append(stack, x) for push.
"Shift" is pop's cousin: it returns the first element and removes it. The idiom is symmetric to pop, but with a hidden cost.
queue[0] reads the head. queue = queue[1:] reslices to skip the first slot. The pointer in the slice header now points one element deeper into the backing array, the length drops by one, and the capacity also drops by one.
O(1) for the operation itself, but the dropped element stays in the backing array until the whole slice is garbage-collected. Repeated shifts also waste the head slots, since the slice never reclaims them. For a real queue, use a ring buffer or a container/list.
The leak risk is real for pointer-heavy types: a shifted-off *Order is still referenced by the backing array and won't be collected until the whole slice is gone. For plain values like int or short strings, the waste is minor.
To remove a range [i, j) from a slice in one step, glue the prefix before i to the suffix from j onward.
The shape is identical to single-element delete; only the right slice changes from pages[i+1:] to pages[j:]. This drops indices i through j-1 inclusive, the standard half-open Go convention.
O(n - j). The suffix shifts left by j - i slots in a single copy call inside append. Much faster than calling single-element delete in a loop.
If you find yourself deleting many elements scattered throughout the slice, prefer filter-in-place over many single deletes. Each single delete is O(n), so a loop of m deletes is O(n*m). One filter pass is O(n).
When a slice is already sorted, removing duplicates is a single linear pass with two indices.
The write index n starts at 1, since the first element is always kept. The loop compares each element to the last one written (tags[n-1]). When they differ, we've found a new unique value; copy it to tags[n] and advance n. When they match, skip.
O(n) time, O(1) extra space. Requires the input to be sorted; dedup on unsorted data needs a map or O(n^2) comparisons.
The pre-loop check handles the empty case so tags[n-1] (tags[0]) doesn't panic. The pattern only works on sorted input because it relies on duplicates being adjacent.
that combines several tricks. It manages a wishlist: adds items, removes a specific item, filters out anything over a price cap, and pops the most recently added item.
The three operations chain naturally. After step 1, the wishlist is Mouse, Monitor, Webcam, Cable. After step 2 (filter out > $100), it's Mouse, Cable. After step 3 (pop), Cable is removed and returned, and the wishlist is just Mouse. Each step reuses the underlying slice without allocating new memory, except where unavoidable.