ZetCode

Go filter slice

last modified October 30, 2026

In this article we show how to filter a slice in Go. A filtering operation walks a slice and keeps only the elements for which a predicate returns true.

A predicate is a function that takes one element and returns a boolean value. Go has no built-in filter function; the operation is written with a loop, with a generic helper, or with the functions from the slices package.

All examples in this article were compiled and run with Go 1.27.0.

Filtering with a for loop

The most direct way to filter a slice is a for loop that appends the matching elements to a second slice.

main.go
package main

import "fmt"

func main() {

    vals := []int{-2, 0, 1, 9, 7, -3, -5, 6}

    var positive []int

    for _, val := range vals {
        if val > 0 {
            positive = append(positive, val)
        }
    }

    fmt.Println(positive)
}

The loop tests every element of vals and copies the ones greater than zero into positive. The original slice is not modified.

$ go run main.go
[1 9 7 6]

A generic Filter function

The same loop can be wrapped in a generic function so that it works with any element type. The result is allocated once with make([]T, 0, len(vals)), which avoids repeated growth of the backing array.

main.go
package main

import "fmt"

func Filter[T any](vals []T, pred func(T) bool) []T {

    out := make([]T, 0, len(vals))

    for _, val := range vals {
        if pred(val) {
            out = append(out, val)
        }
    }

    return out
}

func main() {

    vals := []int{-2, 0, 1, 9, 7, -3, -5, 6}

    positive := Filter(vals, func(n int) bool { return n > 0 })
    even := Filter(vals, func(n int) bool { return n%2 == 0 })

    fmt.Println(positive)
    fmt.Println(even)
}

Filter takes the slice and the predicate, and the type parameter T is inferred from the call. Two different predicates are applied to the same data.

$ go run main.go
[1 9 7 6]
[-2 0 6]

Filtering in place

When the original slice is not needed any more, the matching elements can be moved to the front of the same backing array. A write index n tracks the position of the next kept element.

main.go
package main

import "fmt"

func main() {

    vals := []int{-2, 0, 1, 9, 7, -3, -5, 6}
    backup := vals

    n := 0
    for _, val := range vals {
        if val > 0 {
            vals[n] = val
            n++
        }
    }

    vals = vals[:n]

    fmt.Println("filtered:", vals, "len:", len(vals), "cap:", cap(vals))
    fmt.Println("backup:  ", backup)
    fmt.Println("shared:  ", &vals[0] == &backup[0])
}

No new array is allocated: backup, which still has length 8, sees the compacted values followed by the leftovers in the tail. The capacity of the result stays 8, and an append to it would overwrite the tail of the original.

$ go run main.go
filtered: [1 9 7 6] len: 4 cap: 8
backup:   [1 9 7 6 7 -3 -5 6]
shared:   true

Clearing the freed tail

If the element type contains pointers, the slots beyond the new length keep references to objects that are otherwise unused. The garbage collector cannot free them until the backing array itself goes away, so the tail is cleared with the clear built-in.

main.go
package main

import "fmt"

func main() {

    vals := []int{-2, 0, 1, 9, 7, -3, -5, 6}
    backup := vals

    n := 0
    for _, val := range vals {
        if val > 0 {
            vals[n] = val
            n++
        }
    }

    clear(vals[n:])
    vals = vals[:n]

    fmt.Println("filtered:", vals)
    fmt.Println("backup:  ", backup)
}

clear zeroes the elements after position n in the backing array. For integers the zero value is 0; for strings it is "" and for pointers nil.

$ go run main.go
filtered: [1 9 7 6]
backup:   [1 9 7 6 0 0 0 0]

Filtering strings by length

The len function returns the number of bytes, not characters. For text with non-ASCII letters the two counts differ, and utf8.RuneCountInString must be used when the number of characters matters.

main.go
package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {

    words := []string{"kot", "łuk", "słoń", "dom", "żółw"}

    var threeBytes []string
    var threeRunes []string

    for _, word := range words {
        if len(word) == 3 {
            threeBytes = append(threeBytes, word)
        }
        if utf8.RuneCountInString(word) == 3 {
            threeRunes = append(threeRunes, word)
        }
    }

    fmt.Println("bytes:", threeBytes)
    fmt.Println("runes:", threeRunes)
}

łuk has three characters but four bytes, so it appears only in the rune-based result. Both filters keep kot and dom.

$ go run main.go
bytes: [kot dom]
runes: [kot łuk dom]

Filtering a slice of structs

Elements of a struct slice are copied into the result, so the filtered slice holds independent values.

main.go
package main

import "fmt"

type User struct {
    Name       string
    Occupation string
    Country    string
}

func main() {

    users := []User{
        {"John Doe", "gardener", "USA"},
        {"Roger Roe", "driver", "UK"},
        {"Paul Smith", "programmer", "Canada"},
        {"Lucia Mala", "teacher", "Slovakia"},
        {"Tim Welson", "programmer", "Canada"},
    }

    var programmers []User

    for _, user := range users {
        if user.Occupation == "programmer" {
            programmers = append(programmers, user)
        }
    }

    for _, u := range programmers {
        fmt.Printf("%s (%s)\n", u.Name, u.Country)
    }
}

The loop keeps the users whose Occupation equals programmer and prints each of them with the country in parentheses.

$ go run main.go
Paul Smith (Canada)
Tim Welson (Canada)

Filtering with slices.DeleteFunc

The slices.DeleteFunc function removes all elements for which the function returns true and returns the shortened slice. It works in place, like the manual loop above.

main.go
package main

import (
    "fmt"
    "slices"
)

func main() {

    vals := []int{-2, 0, 1, 9, 7, -3, -5, 6}

    vals = slices.DeleteFunc(vals, func(n int) bool { return n <= 0 })

    fmt.Println(vals, len(vals), cap(vals))
}

The predicate describes the elements to drop, which is the opposite of a filter predicate. The length shrinks to four elements while the capacity remains 8.

$ go run main.go
[1 9 7 6] 4 8

Searching with ContainsFunc and IndexFunc

Sometimes only one element is needed. slices.ContainsFunc reports whether any element matches the predicate, and slices.IndexFunc returns the position of the first match, or -1.

main.go
package main

import (
    "fmt"
    "slices"
    "strings"
)

func main() {

    words := []string{"sky", "forest", "fly", "cup", "wood", "falcon"}

    fmt.Println(slices.ContainsFunc(words, func(w string) bool {
        return strings.HasPrefix(w, "f")
    }))

    fmt.Println(slices.IndexFunc(words, func(w string) bool {
        return strings.HasPrefix(w, "f")
    }))

    fmt.Println(slices.IndexFunc(words, func(w string) bool {
        return strings.HasPrefix(w, "z")
    }))
}

The word forest is at index 1, so the first two calls succeed. No word starts with z, which produces -1.

$ go run main.go
true
1
-1

Nil versus empty results

A filter that declares the result as var out []T returns nil when nothing matches. A slice created with make([]T, 0) is empty but not nil. Both print as [], so the difference is visible only through a comparison with nil or with encoding/json, which encodes a nil slice as null.

main.go
package main

import "fmt"

func positives(vals []int) []int {

    var out []int

    for _, val := range vals {
        if val > 0 {
            out = append(out, val)
        }
    }

    return out
}

func main() {

    fmt.Println(positives([]int{-1, -2}) == nil)
    fmt.Println(positives([]int{1, 2}) == nil)

    out := positives([]int{-1, -2})
    fmt.Printf("%v len=%d nil=%t\n", out, len(out), out == nil)

    empty := make([]int, 0)
    fmt.Printf("%v len=%d nil=%t\n", empty, len(empty), empty == nil)
}

Both slices have length zero, but only the result of the filter without matches is nil. Use make([]T, 0) when the caller should receive an empty slice that can be encoded as a JSON array.

$ go run main.go
true
false
[] len=0 nil=true
[] len=0 nil=false

Filtering map entries

Maps can be filtered with maps.DeleteFunc. The function receives the key and the value, and the key list of the remaining entries can be collected with maps.Keys and sorted with slices.Sorted.

main.go
package main

import (
    "fmt"
    "maps"
    "slices"
)

func main() {

    stock := map[string]int{
        "apple":  12,
        "banana": 0,
        "cherry": 7,
        "damson": 0,
    }

    available := maps.Clone(stock)
    maps.DeleteFunc(available, func(_ string, n int) bool { return n == 0 })

    names := slices.Sorted(maps.Keys(available))

    fmt.Println(names)
    fmt.Println(available["cherry"])
}

maps.Clone keeps the original map intact. Two entries have a zero count and are removed; the remaining keys are sorted so that the output does not depend on map iteration order.

$ go run main.go
[apple cherry]
7

Source

Go slices package - reference
Go maps package - reference

In this article we have filtered slices in Go with loops, a generic helper, and the slices and maps packages.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Go tutorials.