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. Let's see how to do that

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.