ZetCode

Go filter and map

last modified October 30, 2026

In this article we show how to implement filter and map operations in Go. A filter keeps the elements of a collection for which a predicate returns true. A map applies a function to every element and collects the results in a new collection.

Go has no built-in filter or map function for slices. With generics, both can be written once and used with any element type. The examples were compiled and run with Go 1.27.0.

Filtering strings

A filter is a loop with a condition. The predicate is passed as a function value, so the same loop can be reused with different conditions.

main.go
package main

import (
    "fmt"
    "strings"
)

func Filter(data []string, f func(string) bool) []string {

    out := make([]string, 0, len(data))
    for _, v := range data {
        if f(v) {
            out = append(out, v)
        }
    }
    return out
}

func main() {

    words := []string{"war", "water", "cup", "tree", "storm", "wind"}

    res := Filter(words, func(s string) bool {
        return strings.HasPrefix(s, "w")
    })

    fmt.Println(res)
}

The result keeps the order of the input. The function allocates a new slice with enough capacity for every element, so the append operations do not reallocate in this loop.

$ go run main.go
[war water wind]

Filtering structs

The same pattern works with structs. The predicate receives a copy of the element and can inspect any field.

main.go
package main

import "fmt"

type User struct {
    Name       string
    Occupation string
    Country    string
}

func Filter(data []User, f func(User) bool) []User {

    out := make([]User, 0, len(data))
    for _, v := range data {
        if f(v) {
            out = append(out, v)
        }
    }
    return out
}

func main() {

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

    res := Filter(users, func(u User) bool {
        return u.Country == "Slovakia"
    })

    for _, u := range res {
        fmt.Printf("%-14s %s\n", u.Name, u.Occupation)
    }
}

The predicate compares the Country field, and the result contains the two users from Slovakia. The occupation is printed to show that the struct values were copied into the new slice.

$ go run main.go
Lucia Mala     teacher
Tomas Smutny   programmer

A generic filter

A type parameter replaces the element type, so one function serves strings, integers and any other type. The compiler infers T from the argument.

main.go
package main

import (
    "fmt"
    "strings"
)

func Filter[T any](data []T, f func(T) bool) []T {

    out := make([]T, 0, len(data))
    for _, v := range data {
        if f(v) {
            out = append(out, v)
        }
    }
    return out
}

func main() {

    words := []string{"war", "cup", "water", "tree", "storm"}
    fmt.Println(Filter(words, func(s string) bool {
        return strings.HasPrefix(s, "w")
    }))

    nums := []int{-3, -1, 0, 2, 5, 7}
    fmt.Println(Filter(nums, func(n int) bool {
        return n > 0
    }))
}

The two calls use different element types and different predicates. The constraint any is enough because the filter only assigns and compares nothing itself.

$ go run main.go
[war water]
[2 5 7]

Filtering in place

When the original slice is no longer needed, slices.DeleteFunc removes the elements that match a predicate without allocating a second slice. It shifts the remaining elements and returns the shortened slice.

main.go
package main

import (
    "fmt"
    "slices"
)

func main() {

    nums := []int{1, 2, 3, 4, 5, 6, 7, 8}

    evens := slices.DeleteFunc(slices.Clone(nums), func(n int) bool {
        return n%2 != 0
    })

    fmt.Println(evens)
    fmt.Println("original:", nums)
}

slices.Clone protects the original slice in this example. Without the clone, DeleteFunc would overwrite the elements of nums and return a shorter slice that shares the same array.

$ go run main.go
[2 4 6 8]
original: [1 2 3 4 5 6 7 8]

Mapping integers

A map function allocates a result slice of the same length and assigns every transformed value to the same index.

main.go
package main

import "fmt"

func Map(data []int, f func(int) int) []int {

    out := make([]int, len(data))
    for i, v := range data {
        out[i] = f(v)
    }
    return out
}

func main() {

    nums := []int{1, 2, 3, 4, 5}

    squares := Map(nums, func(n int) int {
        return n * n
    })

    fmt.Println(squares)
}

Because the output has a fixed length, the function uses make([]int, len(data)) and indexed assignment instead of append. The input slice is not modified.

$ go run main.go
[1 4 9 16 25]

Mapping strings

An existing function can be used as the mapping function when its signature matches. strings.ToUpper takes and returns a string, so it can be passed directly.

main.go
package main

import (
    "fmt"
    "strings"
)

func Map(data []string, f func(string) string) []string {

    out := make([]string, len(data))
    for i, v := range data {
        out[i] = f(v)
    }
    return out
}

func main() {

    words := []string{"war", "water", "cup", "tree", "storm"}

    fmt.Println(Map(words, strings.ToUpper))
}

Passing a named function is the same as passing an anonymous function with the same body. The elements are transformed independently, so the order is preserved.

$ go run main.go
[WAR WATER CUP TREE STORM]

A generic map

A generic map has two type parameters: the input element type T and the output element type U. This allows a map that changes the type, for example from an integer to its decimal representation.

main.go
package main

import (
    "fmt"
    "strconv"
)

func Map[T, U any](data []T, f func(T) U) []U {

    out := make([]U, 0, len(data))
    for _, v := range data {
        out = append(out, f(v))
    }
    return out
}

func main() {

    nums := []int{1, 2, 3, 4, 5}

    labels := Map(nums, func(n int) string {
        return "#" + strconv.Itoa(n)
    })

    fmt.Println(labels)
    fmt.Printf("%T\n", labels)
}

The compiler infers T from the input slice and U from the return type of the function. The printed type confirms that the result is a slice of strings.

$ go run main.go
[#1 #2 #3 #4 #5]
[]string

Combining filter and map

Filter and map compose naturally: the output of one is the input of the other. The intermediate slice exists only between the two calls.

main.go
package main

import (
    "fmt"
    "strconv"
    "strings"
)

func Filter[T any](data []T, f func(T) bool) []T {

    out := make([]T, 0, len(data))
    for _, v := range data {
        if f(v) {
            out = append(out, v)
        }
    }
    return out
}

func Map[T, U any](data []T, f func(T) U) []U {

    out := make([]U, 0, len(data))
    for _, v := range data {
        out = append(out, f(v))
    }
    return out
}

func main() {

    nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    evens := Filter(nums, func(n int) bool { return n%2 == 0 })
    labels := Map(evens, func(n int) string { return strconv.Itoa(n * n) })

    fmt.Println(strings.Join(labels, " "))
}

The filter keeps the even numbers and the map turns each of them into its square written as text. Joining the result produces one line with the values in the original order.

$ go run main.go
4 16 36 64 100

Source

Go generics tutorial
Go slices package - reference

In this article we have implemented filter and map operations in Go. We filtered strings and structs, wrote generic versions for any type, used slices.DeleteFunc for an in-place filter, mapped integers and strings, changed the element type in a map and combined both operations.

Author

I am Jan Bodnar, a programmer and technical writer. I have been writing programming tutorials at ZetCode since 2007 and have published eight e-books. See about the author.

List all Go tutorials.