Implementing a Generic Filter Function in Go

This article will demonstrate the implementation of a generic slice filter function using the new type parameters syntax


Got comments?
Implementing a Generic Filter Function in Go

Please, help me feature this story on Hacker News.

For those unaware of the state of generics in Go, there has been some recent indication that type parameters as they are also known, are going to make it into version 1.18 of the language:

No change in consensus, so accepted. 🎉

In fact, at the time of this writing, type parameters have already been merged into the master branch, and one can already get a feel in v1.17.

Without taking a side (yet 😉), I thought I could slowly start introducing the reader to generic type parameters, hoping that they would be used sparingly and not turn into the biggest foot-gun in the language's history.

This is not an intro to type parameters. I would point the reader to the official proposal's page, as well as to Robert Griesemer's recent GopherCon talk. I promise to do a proper starter to generics, but for now, let's assume that the reader understands the proposed syntax addition to the language.

Suppose we have a function called filter that we want to use to reduce an input slice (of unknown items types) using a function that decides whether an element should remain in the final output or not. Using the current Go syntax, such a function's declaration would look like this:

func filter(items []interface{}, fn func(index int, item interface{}) bool) []interface{} {
    filteredItems := []interface{}{}
    for index, value := range items {
        if fn(index, value) {
            filteredItems = append(filteredItems, value)
        }
    }
    return filteredItems
}

To reduce the visual clutter, let's alias interface{} to any. Our function's declaration should hopefully become one idea more readable now:

type any = interface{}

func filter(items []any, fn func(index int, item any) bool) []any {
    filteredItems := []any{}
    for index, value := range items {
        if fn(index, value) {
            filteredItems = append(filteredItems, value)
        }
    }
    return filteredItems
}

From interface{} to type parameters

I did not choose the name any by coincidence. Along with comparable (subject to another discussion) any is one of the two new keywords that will enter the language. It basically has the same meaning as interface{} and if we start writing lots of generic Go code, we will be seeing it many times. It will be used as a constraint when writing type parameter expressions.

What are constraints? Without going into detail, constraints make sure that elements of a generic type always fulfill a particular interface type. In alignment with Go’s philosophy, adding a constraint next to a type parameter declaration is mandatory.

func foo[T SomeInterface](items []T)

This way, by glancing at the type parameter, one can easily say whether the generic type needs to implement a given interface.

In a case like ours, where we cannot enforce a particular interface on it, we use any, which is a synonym for interface {}. The role of a constraint will be played by our second argument (the closure, a.k.a. predicate).

The final result

func filter[T any](items []T, fn func(item T) bool) []T {
    filteredItems := []T{}
    for _, value := range items {
        if fn(value) {
            filteredItems = append(filteredItems, value)
        }
    }
    return filteredItems
}

func main() {
    nums := []int{1, 2, 3, 4, 5}
    evenNums := Filter(nums, func(num int) bool { return num%2 == 0 })
    fmt.Println(evenNums)

Playground link

For comparison, I am adding the non-type-params version.

func filter(items []interface{}, fn func(index int, item interface{}) bool) []interface{} {
    filteredItems := []interface{}{}
    for index, value := range items {
        if fn(index, value) {
            filteredItems = append(filteredItems, value)
        }
    }
    return filteredItems
}

func main() {
    var nums []interface{}
    nums = append(nums, 1, 2, 3, 4, 5)
    evenNums := filter(nums, func(index int, num interface{}) bool { return num.(int)%2 == 0 })
    
    // of course, items now need to be type-asserted every time you use them
    fmt.Printf("%d", evenNums[0].(int) + 2)
}
GopherCon 2020: Robert Griesemer - Typing [Generic] Go | YouTube