1. What clear does and why you need it
If you’ve only just gotten used to append, copy, and s[a:b], then clear can initially look like “another magic nail” in the Go toolbox. But in reality, clear addresses a very practical pain point: we know how to reduce len (do s = s[:newLen]), but that does not erase values from the backing array. That means we can “hide” elements, but they’re still sitting there—like forgotten pizza in the fridge: you don’t see it, but a day later the smell will remind you.
clear is a built-in function. For a slice, it writes the zero value into the elements of the range. It’s important to lock this in right away: clear does not change len or cap. It doesn’t “cut” anything off; it just “wipes the floor.”
This philosophy is generally typical for Go: the language has often added small built-in mechanisms that remove repetitive boilerplate and make behavior more predictable. Historically, a similar story happened with append: people used to write manual “addToList” helpers with make + copy, and then it got packaged into one clear tool.
Mini example: clear does not make a slice empty
package main
import "fmt"
func main() {
s := []int{10, 20, 30}
clear(s)
fmt.Println(s) // [0 0 0]
fmt.Println(len(s)) // 3
}
2. Zero value and clearing references
Now comes a moment where Go behaves like a strict math teacher: “Don’t make things up, memorize the definitions.” clear writes the zero value for the element type.
For int it is 0, for bool it is false, for string it is "". For “reference-like” types (for example, []int, map[string]int) the zero value usually looks like nil. And this is critical: when you zero an element of a reference type, you remove the reference to the data. And that’s no longer about “pretty output”; it’s about memory and the garbage collector (GC).
We’re not going deep into GC yet. A very practical model is enough:
If somewhere in the program there is still a reference to the data, the GC considers the data still needed and does not free the memory.
clear is one way to carefully remove such references when you have logically “deleted” an element from a collection.
Table: clear for different types
| Element type T | Zero value after clear | What it means in practice |
|---|---|---|
|
|
“Erased the number” (no effect on memory beyond the cell itself) |
|
|
“Erased the flag” |
|
|
Removed the reference to the string data (important for memory) |
|
|
Removed the reference to another slice’s backing array |
|
|
Removed the reference to the hash table |
We won’t dive into map right now; just note that clear is not “only for []T.”
Mini example: clear clears part of a range
package main
import "fmt"
func main() {
s := []string{"A", "B", "C", "D"}
clear(s[1:3])
fmt.Println(s) // [A D]
// more precisely: ["A" "" "" "D"]
}
3. Why reducing len does not erase the tail
Now we’re getting to the main reason why people often do clear after deletion. One of the most common “ghost bugs” with slices lives here.
A slice has len and cap. len tells you what part of the backing array is visible right now. cap tells you how far the backing array extends from the start of that slice.
What lies beyond len but still within cap is usually called the “tail.” You can’t see it through normal indexing s[i], because indices are checked against len. But the tail really exists in the backing array’s memory.
Here’s a model diagram:
backing array: [ A ][ B ][ C ][ D ][ ? ][ ? ]
slice s: ^----------------^
len=4 cap=6
tail: [ ? ][ ? ] (part of the backing array, but beyond len)
And now the key trick: if you do s = s[:2], the backing array doesn’t “shrink” and doesn’t “get erased.” You only changed the “viewport.”
Mini example: you can see the tail by extending the slice
This example looks a bit artificial (because people rarely write s[:cap(s)] in real code), but it demonstrates the mechanics perfectly.
package main
import "fmt"
func main() {
s := []string{"A", "B", "C", "D"}
s = s[:2] // we only see A and B
t := s[:cap(s)] // extend visibility to cap
fmt.Println(t) // [A B C D]
}
Moral: “deleting” an element by reducing len is not the same as “erasing” the value in the backing array.
By the way, in previous topics you may have already seen the tool for controlling cap via the full slice expression s[a:b:c]. It was added specifically to manage access to the backing array and the tail more safely.
4. Why clear the tail: bugs and memory
It’s easy to say: “So what, the tail can just sit there.” But there are two reasons why you should clear the tail after deletion—and both bite regularly in practice.
The first problem is logical. After deletion, you often expect the “deleted” value to be gone everywhere. But if somewhere a reslice happens by mistake (or you passed a subslice with a larger cap and someone did append), the old value can unexpectedly “resurface.” That’s not magic; it’s just the tail you didn’t wipe.
The second problem is memory. It’s especially important when slice elements hold references to large data: strings, other slices, maps. If you “deleted” an element but its reference is still sitting in the backing array tail, the GC will continue to think the data is still needed, and the memory may not be freed. In the end, your code logically “removed” the data, but memory-wise—“not quite.”
You can think of it this way: reducing len is “we no longer use it,” and clear is “and there are no references left either.”
5. Deleting an element: copy + clear + a new len
Now we’ll draw the main practical conclusion: after deletion (especially stable deletion, where we shift the tail left with copy) a duplicate remains at the end. That “last element of the old length” is no longer needed logically, but physically the old value will still be there.
So the typical pattern looks like this:
- Shift elements left (copy).
- Zero the last element of the old length (clear over a single-element range).
- Shorten len (s = s[:len(s)-1]).
Mini example: stable remove of one element
package main
import "fmt"
func removeAtStable(s []string, i int) []string {
if i < 0 || i >= len(s) {
return s
}
copy(s[i:], s[i+1:]) // shift the tail left
clear(s[len(s)-1 : len(s)]) // zero the last slot
return s[:len(s)-1] // reduce len
}
func main() {
tasks := []string{"eat", "sleep", "code", "repeat"}
tasks = removeAtStable(tasks, 1)
fmt.Println(tasks) // [eat code repeat]
}
Note clear(s[len(s)-1 : len(s)]): this is a range of length 1. Yes, it looks a bit “bureaucratic,” but it’s very explicit: “clear exactly one element.”
If you want it shorter (and you’re confident you understand the boundaries), you can write clear(s[len(s)-1:]), but that’s a different idea: “clear the tail up to cap.” In our situation, you usually want to clear exactly the “freed slot.”
6. Deleting a range: clear s[newLen:oldLen]
Deleting a range [i:j) is the same thing, just with a wider “hole.” We close it with the tail via copy(s[i:], s[j:]). After that, a few “extra” elements remain at the end (exactly j-i of them)—and those are exactly what you should clear.
It’s convenient to think in terms of two lengths here: the old oldLen and the new newLen. Until you shorten len, the clear range is easy to specify explicitly.
Mini example: delete range + tail cleanup
package main
import "fmt"
func deleteRange(s []string, i, j int) []string {
if i < 0 {
i = 0
}
if j > len(s) {
j = len(s)
}
if i >= j {
return s
}
oldLen := len(s)
moved := copy(s[i:], s[j:]) // how many actually moved
newLen := i + moved
clear(s[newLen:oldLen]) // clear exactly the “freed” part
return s[:newLen]
}
func main() {
s := []string{"A", "B", "C", "D", "E", "F"}
s = deleteRange(s, 2, 5)
fmt.Println(s) // [A B F]
}
What matters here is that clear is done before s = s[:newLen] (or with oldLen preserved). Otherwise you lose convenient access to the “old tail” range.
7. Example: a mini task manager
Let’s continue our mini app storyline. Suppose we have a very simple task manager: the task list is stored as a []string. We can print tasks, add tasks, and delete by index.
Now we’ll add deletion in a way that doesn’t leave “dangling” references in the backing array tail.
Step 1: printing tasks
package main
import "fmt"
func printTasks(tasks []string) {
for i, t := range tasks {
fmt.Printf("%d) %s\n", i, t)
}
}
func main() {
tasks := []string{"read", "write", "delete tail carefully"}
printTasks(tasks)
// 0) read
// 1) write
// 2) delete tail carefully
}
Step 2: delete by index with clear
package main
import "fmt"
func removeTask(tasks []string, i int) []string {
if i < 0 || i >= len(tasks) {
return tasks
}
copy(tasks[i:], tasks[i+1:])
clear(tasks[len(tasks)-1 : len(tasks)])
return tasks[:len(tasks)-1]
}
func main() {
tasks := []string{"read", "write", "sleep", "repeat"}
tasks = removeTask(tasks, 2)
fmt.Println(tasks) // [read write repeat]
}
The nice part here is that the function returns a new slice. This directly matches the “length rule”: if an operation changes len, the caller must receive the updated slice. Otherwise you almost certainly end up with the weird state of “it changed inside, but not outside.”
Process diagram for deletion and cleanup
Sometimes it’s easier for the brain to see it once than read it five times. Here’s a flowchart for “stable remove + clear tail”:
flowchart TD
A[We have a slice tasks, len=n] --> B["Shift the tail: copy(tasks[i:], tasks[i+1:])"]
B --> C[A duplicate of the last element appears at the end]
C --> D["Clear the last slot: clear(tasks[n-1:n])"]
D --> E["Shorten len: tasks = tasks[:n-1]"]
E --> F[Done: order is preserved, the tail holds no references]
8. Common mistakes with clear after deletion
Mistake #1: expecting clear to change the length.
A very common logical trap: you do clear(s) and expect the “slice became empty.” But clear is not about size at all. It’s about values. If you need a slice that’s “empty by length,” that’s s = s[:0]. If you need “empty by values, but the same length,” that’s exactly clear(s).
Mistake #2: clearing the wrong range because of [a:b) boundaries.
When you clear the tail, it’s almost always about a half-open interval. For example, the last element of the old length is [oldLen-1 : oldLen], not [oldLen-1 : oldLen-1] (that’s empty) and not [oldLen : oldLen+1] (panic). The most reliable approach early on is to save oldLen and write ranges explicitly, even if it looks a bit longer.
Mistake #3: forgetting that deletion is at least two actions, and clear is separate.
Sometimes people write copy(s[i:], s[i+1:]) and think they “deleted.” But the length is still the old one, and the last element will be duplicated. Then they add s = s[:len(s)-1] and consider it done. For []int this is often “fine,” but for []string or [][]byte you silently leave references in the tail. If you reduced len in code, it’s a useful habit to ask yourself: “Do I need to wipe the tail?”
Mistake #4: calling clear after you’ve lost access to the tail.
If you first do s = s[:newLen] and don’t store the old length anywhere, then clearing the “freed part” via clear(s[newLen:oldLen]) is no longer possible—you don’t have oldLen. Yes, you can clear up to cap, but that’s a different solution and not always what you intended. In delete helpers it’s often easiest to remember oldLen, then clear, and only then change len.
Mistake #5: ignoring memory retention as if it were a myth.
When the data is small, you won’t notice the problem. When the slice holds large strings or large subslices, “deleted but didn’t clear the tail” turns into a “feels like a leak” situation: memory doesn’t come back, the process grows, and you get sad and suspect Go of all mortal sins. Usually it’s not Go’s fault; it’s that same tail you were too lazy to zero.
GO TO FULL VERSION