Tags:

Golang generic functions – mimic Javascript methods: Map, Contains, Filter, Reduce

Published
98

Go Generics

I’ve been working on some Go projects lately, and I gotta say – there’s been a lot of duplicated code going on.

It’s weird, because I’ve never seen so much of it in my previous projects.

It’s like the more I work with Go, the more I realize that some of its built-in functions for slices can be a bit of a productivity killer.

But, you know what they say – necessity is the mother of invention. So, I’ve been experimenting with Go’s new generic features (which they finally added in version 1.18 last February), and let me tell you, it’s been a game changer.

In this article, I wanted to share some of my learnings with you.

Specifically, I’m going to show you how to use Go’s new generic features to recreate some of the most useful methods from the JavaScript standard library, like Map, Contains, Filter, and Reduce.

With these methods, you can kiss those duplicated code headaches goodbye, and work with slices in Go more efficiently. Plus, your code will be much more readable and maintainable, which is always a plus.

Use Generic to mimic Javascript methods

Contains

Go has a package named golang.org/x/exp – which already provided some methods we need, included ContainsFunc:

func ContainsFunc[E any](s []E, f func(E) bool) bool

Usage of ContainsFuncto check if a slice contains a struct with a custom comparison:

package main

import (
	"fmt"

	"golang.org/x/exp/slices"
)

type Person struct {
	Name string
	Age  int
}

func main() {
	people := []Person{
		{"Alice", 25},
		{"Bob", 30},
		{"Charlie", 35},
	}

	hasCharlie := slices.ContainsFunc(people, func(p Person) bool {
		return p.Name == "Charlie"
	})

	fmt.Printf("people contains a person named Charlie? %t\n", hasCharlie)
}

Filter

Go doesn’t have arrow functions like JavaScript, but it does support passing functions as a parameter. In C# or Java, we called it a predicate because of the boolean returning.

func FilterFunc[E any](s []E, f func(E) bool) []E {
	var filtered []E
	for _, x := range s {
		if f(x) {
			filtered = append(filtered, x)
		}
	}
	return filtered
}

Map

Map is a helpful function to turn a slice into another slice with a different type.

func MapFunc[E, T any](s []E, f func(E) T) []T {
	mapped := make([]T, len(s))
	for i, x := range s {
		mapped[i] = f(x)
	}
	return mapped
}

Reduce

Ever used the Reduce method in JavaScript to calculate the sum or average of an array, find the max or min value, or flatten a multi-dimensional array? It’s a handy tool, for sure.

Well, you’ll be happy to know that with Go’s new generic features, we can mimic that same functionality. Here’s a simple implementation to get you started:

func Reduce[E any, R any](s []E, f func(R, E) R, initial R) R {
    result := initial
    for _, v := range s {
        result = f(result, v)
    }
    return result
}

Examples

With all of these functions, we can use them like below:

package main

import (
	"fmt"
	"strings"

	"golang.org/x/exp/slices"
)

type Person struct {
	Name string
	Age  int
}

func FilterFunc[E any](s []E, f func(E) bool) []E {
	var filtered []E
	for _, x := range s {
		if f(x) {
			filtered = append(filtered, x)
		}
	}
	return filtered
}
func MapFunc[E, T any](s []E, f func(E) T) []T {
	mapped := make([]T, len(s))
	for i, x := range s {
		mapped[i] = f(x)
	}
	return mapped
}
func Reduce[E any, R any](s []E, f func(R, E) R, initial R) R {
	result := initial
	for _, v := range s {
		result = f(result, v)
	}
	return result
}
func main() {
	people := []Person{
		{"Alice", 25},
		{"Bob", 30},
		{"Charlie", 35},
		{"David", 40},
		{"Eve", 45},
	}

	// Use Map to capitalize all names
	upperCaseNames := MapFunc(people, func(p Person) string {
		return strings.ToUpper(p.Name)
	})
	fmt.Println("Capitalized names:", upperCaseNames)

	// Use Filter to find people over 30 years old
	over30 := FilterFunc(people, func(p Person) bool {
		return p.Age > 30
	})
	fmt.Println("People over 30:", over30)

	// Use Reduce to calculate the total age of all people
	totalAge := Reduce(people, func(acc int, p Person) int {
		return acc + p.Age
	}, 0)
	fmt.Println("Total age:", totalAge)

	// Use ContainsFunc to check if a person with the name "Alice" exists
	containsAlice := slices.ContainsFunc(people, func(p Person) bool {
		return p.Name == "Alice"
	})
	fmt.Println("Contains Alice?", containsAlice)
}

Output:

ShellScript
> go run main.go
Capitalized names: [ALICE BOB CHARLIE DAVID EVE]
People over 30: [{Charlie 35} {David 40} {Eve 45}]
Total age: 175
Contains Alice? true

Conclusion

Since the introduction of generics in Go 1.18, we can finally use some of the most useful methods from the JavaScript standard library – like Map, Contains, Filter, and Reduce. These methods make it easier and faster to work with slices in Go, so we can write more readable and maintainable code.

Of course, the possibilities for creating custom generic functions are endless. But keep in mind that you’ll need to balance readability, maintainability, and performance when designing your functions.

Overall, I’m stoked about how generics have made Go even more powerful and flexible. So go ahead and dive in – happy coding, folks!

Keywords:

Idiomatic Replacement for map/reduce/filter/etc, Contains method for a slice, filter a slice in Go, Clone Js array methods to Go