Skip to main content
Preslav Rachev
  1. My Writings /Programming /

Implementing a Generic Filter Function in Go

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

Image Credits: Tyler Nix / Unsplash

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
}
Do you like my writing? Don’t forget to follow me on Twitter.

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)
}

Have something to say? Join the discussion below 👇

Want to explore instead? Fly with the time capsule 🛸

You may also find these interesting

Between Go and Elixir

·10 mins

Reason wanted me to make a choice, and I am so glad I didn’t. Because the more I kept delving into both Elixir and Go, the more I found out how complementary the two can be to one another.