
Filter a slice in Golang
Slices are a cornerstone of Go programming, offering a flexible way to work with sequences of data. One common task is filtering a slice—selecting elements that meet specific criteria. Over the years, Go developers have relied on basic loops for this, but with recent releases, filtering has got easier. In this post, we’ll explore how to filter slices, starting with classic approaches and moving into the latest stable features.
The Old Ways: Loops and Manual Filtering
Let’s start with how filtering was traditionally done in Go. Suppose we have a slice of integers and want to keep only the even numbers. Here’s a simple, pre-generics approach:
package main
import "fmt"
func main() {
numbers := []int{1, 2, 3, 4, 5, 6}
var even []int
for _, n := range numbers {
if n%2 == 0 {
even = append(even, n)
}
}
fmt.Println(even) // Output: [2 4 6]
}
You can try this example in the Go playground here.
This works fine: we iterate over the slice, check each element, and build a new slice with append
. It’s straightforward but repetitive—every filtering task requires writing a similar loop. Another old trick was in-place filtering, reusing the original slice’s backing array to avoid allocation:
package main
import "fmt"
func main() {
numbers := []int{1, 2, 3, 4, 5, 6}
n := 0
for _, val := range numbers {
if val%2 == 0 {
numbers[n] = val
n++
}
}
numbers = numbers[:n]
fmt.Println(numbers) // Output: [2 4 6]
}
You can try this example here.
This is more memory-efficient but modifies the original slice, which isn’t always desirable. These methods served us well, but they lack the elegance and reusability of functional-style filtering found in other languages.
Enter Generics (Go 1.18+): Reusable Filtering
Go 1.18 introduced generics, letting us write type-safe, reusable functions. Here’s a generic filter function that works with any slice type:
package main
import "fmt"
func filter[T any](s []T, predicate func(T) bool) []T {
result := make([]T, 0, len(s)) // Pre-allocate for efficiency
for _, v := range s {
if predicate(v) {
result = append(result, v)
}
}
return result
}
func main() {
numbers := []int{1, 2, 3, 4, 5, 6}
even := filter(numbers, func(n int) bool { return n%2 == 0 })
fmt.Println(even) // Output: [2 4 6]
words := []string{"cat", "dog", "bird", "rat"}
short := filter(words, func(w string) bool { return len(w) <= 3 })
fmt.Println(short) // Output: [cat dog rat]
}
You can try that here.
Generics made filtering more DRY (Don’t Repeat Yourself). We define the logic once and reuse it across types. However, this still involves manual iteration—nothing in the standard library provided a built-in filter until later.
Go 1.23: Leveraging the slices
Package and Iterators
Go 1.21 introduced the slices
package, which became a game-changer for slice operations. By Go 1.23, it’s matured further, integrating with iterators (introduced in Go 1.23 as part of the iter
package). Let’s filter a slice using these tools:
package main
import (
"fmt"
"slices"
)
func main() {
numbers := []int{1, 2, 3, 4, 5, 6}
even := slices.Collect(func(yield func(int) bool) {
for _, n := range numbers {
if n%2 == 0 {
if !yield(n) {
return
}
}
}
})
fmt.Println(even) // Output: [2 4 6]
}
You can see that here.
Here, slices.Collect
gathers values yielded by an iterator. The iterator function uses a yield
callback, which we call only for elements passing our condition. This approach is more functional and aligns with Go’s iterator support. It’s concise and leverages the standard library, reducing boilerplate.
The slices
package also offers ContainsFunc
, which isn’t filtering per se but can help in related tasks. For filtering, Collect
with a custom iterator is a standout feature in Go 1.23.
Comparing the Approaches
- Old Loops: Simple, explicit, but verbose and not reusable. Good for one-off tasks or when performance tweaking (like in-place filtering) is critical.
- Generics: Reusable and type-safe, but still requires custom code. A solid middle ground pre-1.23.
- Go 1.23 Iterators: Functional and library-supported, though the iterator syntax might feel unfamiliar to some Gophers.
Performance Tips
Regardless of the method, consider these optimizations:
- Pre-allocate the result slice with
make([]T, 0, len(s))
to minimize reallocations. - For in-place filtering, reuse the backing array when order doesn’t matter and mutation is acceptable.
- With iterators, trust the standard library to handle optimizations, but profile if performance is critical.
Conclusion
Filtering slices in Go has evolved significantly. From manual loops to generics in Go 1.18, and now to iterators in Go 1.23, the language balances simplicity, power, and performance. Whether you’re maintaining legacy code or building something new, these proven methods have you covered. Try out the slices.Collect
approach in Go 1.23—it’s a taste of modern Go at its best.