It's been a while since Go 1.22 was released in February 2024. Among the many changes and improvements in the release, there are some interesting updates in the slices
package.
The slices
package is a utility package that provides a set of functions to manipulate slices, which was added to the standard library in Go 1.21.
The Delete()
function is one of the methods in the slices
package that underwent significant changes in Go 1.22.
Let's delve into the changes in the Delete()
function in Go 1.22.
what does slices.Delete() do?
The signature of Delete()
is as follows:
func Delete[S ~[]E, E any](s S, i, j int) S
As the name suggests, Delete()
is used to delete elements from a slice. It takes three arguments:
s
: The input slice from which elements need to be deleted.i
: The starting index of the range of elements to be deleted.j
: The ending index of the range of elements to be deleted.
Delete()
returns a new slice with the elements in the range [i, j)
removed.
The sample code provided in the official documentation demonstrates how to use Delete()
:
package main
import (
"fmt"
"slices"
)
func main() {
letters := []string{"a", "b", "c", "d", "e"}
letters = slices.Delete(letters, 1, 4)
fmt.Println(letters)
}
// output:
// [a e]
As you can see, the elements from index 1 to 4 (excluding 4) are removed from the letters
slice.
One aspect worth noting here is the value held by letters
after being passed as an argument to Delete()
.
slices.Delete() in Go 1.21
To answer this question, let's modify the code slightly and observe the outcome. First, let's run the code with Go 1.21.
package main
import (
"fmt"
"slices"
)
func main() {
before := []string{"a", "b", "c", "d", "e"}
after := slices.Delete(before, 1, 4)
fmt.Println(before)
fmt.Println(after)
}
// output:
// [a e c d e]
// [a e]
The value of after
is [a e]
, which seems correct. However, the value of before
after being passed to Delete()
is [a e c d e]
, which might be confusing.
To understand what's happening, let's examine the implementation of Delete()
in Go 1.21.
func Delete[S ~[]E, E any](s S, i, j int) S {
_ = s[i:j] // bounds check
return append(s[:i], s[j:]...)
}
First, Delete()
performs a bounds check on the input slice s
with the range [i, j)
to ensure it's within bounds of underlying array of s
.
Then, it uses append()
to create a new slice by combining elements from the beginning of the slice up to index i
and the elements from index j
to the end of the slice.
While this implementation is straightforward, it modifies the input slice s
directly.
append() and the input slice
To understand this behavior of Delete()
, it's crucial to recognize that append()
modifies the input slice when the capacity of the slice's underlying array is sufficient to accommodate the appended elements.
If you pass a slice to the append()
function, Go checks the capacity of the underlying array. If sufficient, append()
adds the new elements to the existing slice.
Conversely, if the capacity is insufficient, append()
creates a new underlying array with a larger capacity and copies the elements from the old array to the new one, leaving the input slice unaffected.
Let's see how append()
works with underlying arrays.
package main
import (
"fmt"
)
func main() {
s1 := []string{"a", "b", "c", "d", "e"}
s2 := append(s1[:3], "x")
fmt.Printf("s1:%v, len:%d, cap%d\n", s1, len(s1), cap(s1))
fmt.Printf("s2:%v, len:%d, cap%d\n", s2, len(s2), cap(s2))
s3 := []string{"f", "g", "h", "i", "j"}
s4 := append(s3[:3], "x", "y", "z")
fmt.Printf("s3:%v, len:%d, cap%d\n", s3, len(s3), cap(s3))
fmt.Printf("s4:%v, len:%d, cap%d\n", s4, len(s4), cap(s4))
}
// output:
// s1:[a b c x e], len:5, cap5
// s2:[a b c x], len:4, cap5
// s3:[f g h i j], len:5, cap5
// s4:[f g h x y z], len:6, cap10
Notice that when append()
is called with s1[:3]
and "x"
, the underlying array of s1
is modified since its capacity is sufficient.
However, when append()
is called with s3[:3]
and the three new elements "x"
, "y"
, and "z"
, a new underlying array which requires the capacity of 6 at least is created due to insufficient capacity, leaving s3
unchanged.
This is how slices.Delete()
behaves unexpectedly in Go 1.21.
slices.Delete() in Go 1.22
The behavior of Delete()
in Go 1.21 was deemed suboptimal by many Go developers. Consequently, in Go 1.22, the implementation of Delete()
was altered for better predictability.
The implementation of Delete()
in Go 1.22 is as follows:
func Delete[S ~[]E, E any](s S, i, j int) S {
_ = s[i:j:len(s)] // bounds check
if i == j {
return s
}
oldlen := len(s)
s = append(s[:i], s[j:]...)
clear(s[len(s):oldlen]) // zero/nil out the obsolete elements, for GC
return s
}
While this implementation may appear more complex, let's break down the changes.
The bounds check now includes
len(s)
in addition toi
andj
, ensuring the range[i, j)
is within the bounds of s. In Go 1.21,Delete()
failed to perform this check whenj
exceeded the length of the slice.The if
i
==j
condition was introduced to return the input slice s when no elements need to be deleted.The
clear()
function was introduced to zero out obsolete elements in the slice. This aids the garbage collector in reclaiming memory occupied by these elements, preventing potential memory leaks.
Let's see how the new Delete()
behaves with the same input as before.
package main
import (
"fmt"
"slices"
)
func main() {
before := []string{"a", "b", "c", "d", "e"}
after := slices.Delete(before, 1, 4)
fmt.Println(before)
fmt.Println(after)
}
// output:
// [a e ]
// [a e]
As observed, the value of after
remains [a e]
, as in Go 1.21. However, the value of before
is now [a e ]
, unlike Go 1.21, where it was [a e c d e]
.
This discrepancy is because the obsolete elements are zeroed out as a result of the clear()
call in the new implementation of Delete()
.
It's worth noting that other functions in the slices package that shrink slice size (Delete, DeleteFunc, Compact, CompactFunc, and Replace) also underwent similar changes in Go 1.22.
Conclusion
In conclusion, slices.Delete()
underwent intriguing changes in Go 1.22. The new implementation of Delete()
ensures that the input slice is not unexpectedly modified and aids the garbage collector in reclaiming memory occupied by obsolete elements.
I hope this article helps you understand the changes in Delete()
in Go 1.22, how it affects your code, and sheds light on how append()
interacts with underlying arrays.