ZetCode

Go generics

last modified November 14, 2022

In this article we show how to work with generics in Golang.

$ go version
go version go1.18.1 linux/amd64

We use Go version 1.18.

With generics, we can write code that can be used with various types. Functions and other types can be written to use any of a set of types. Generics help reduce code duplication.

Go uses parameter types for generics. They are specified inside square brackets [].

func Println[T any](v T) {
    fmt.Println(v)
}

By convention, generic parameter types are specified with uppercase letters, typically T, K, or V.

Go generics example

In the first example, we define a simple generic function.

main.go
package main

import (
    "fmt"
)

func Println[T any](v T) {
    fmt.Println(v)
}

func main() {
    Println[string]("an old falcon")
    Println[int](23)
    Println[float64](3.34)
    Println[bool](true)
    Println[[]int]([]int{1, 2, 3, 4, 5})
}

Our custom Println function can accept any type and print it.

Println[string]("an old falcon")
Println[int](23)
Println[float64](3.34)
Println[bool](true)
Println[[]int]([]int{1, 2, 3, 4, 5})

We print a string value, an int64 value, a float64 value, a bool value and a integer slice value.

$ go run main.go
an old falcon
23
3.34
true
[1 2 3 4 5]

Go generics omit type

In many cases, we can omit the type when calling the generic function. The compiler will infer the type from the function arguments.

main.go
package main

import (
    "fmt"
)

func Println[T any](v T) {
    fmt.Println(v)
}

func main() {
    Println("an old falcon")
    Println(23)
    Println(3.34)
    Println(true)
    Println([]int{1, 2, 3, 4, 5})
}

In the program we omit the parameter type declarations when calling the Println function.

$ go run main.go
an old falcon
23
3.34
true
[1 2 3 4 5]

Go generic union type

We can restrict the generic type parameter to the types in the union.

main.go
package main

import (
    "fmt"
)

func Println[T string | int](v T) {
    fmt.Println(v)
}

func main() {
    Println("an old falcon")
    Println(23)
    // Println(3.34)
    // Println(true)
    // Println([]int{1, 2, 3, 4, 5})
}

In the program, the Println function can accept either strings or integers.

$ go run main.go
an old falcon
23

Go generics tilde

The ~ tilde token is used in the form ~T to denote the set of types whose underlying type is T.

main.go
package main

import (
    "fmt"
)

type mystring string

func Println[T ~string | int](v T) {
    fmt.Println(v)
}

func main() {
    Println("an old falcon")
    Println(23)
    Println(mystring("rainy day"))
}

In the program, we have a custom mystring type. With ~string syntax, we tell the compiler to include any type that approximates to string.

$ go run main.go
an old falcon
23
rainy day

Go generic filter function

The filter function processes a collection and produces a new collection containing exactly those elements for which the given predicate returns true.

In the next example, we create a generic version of the filter function.

main.go
package main

import (
    "fmt"
    "strings"
)

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

    fltd := make([]T, 0, len(data))

    for _, e := range data {
        if f(e) {
            fltd = append(fltd, e)
        }
    }

    return fltd
}

func main() {

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

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

    fmt.Println(res)

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

    res2 := filter(vals, func(e int) bool {
        return e > 0
    })

    fmt.Println(res2)
}

In the program we use a generic filter function to filter strings and integers.

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

    fltd := make([]T, 0, len(data))

    for _, e := range data {
        if f(e) {
            fltd = append(fltd, e)
        }
    }

    return fltd
}

The filter function builds a new slice which includes only elements that satisfy the given condition. The function works on parameter type T with constraint any. It takes a collection and a predicate function as parameters. We call the predicate on each element and add it to the fltd slice if it matches the predicate's condition.

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

Here we filter out all words that start with 'w'.

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

Go generic ForEach function

In the next program, we create a generic ForEach function.

main.go
package main

import "fmt"

func ForEach[T any](data []T, f func(e T, i int, data []T)) {

    for i, e := range data {
        f(e, i, data)
    }
}

func main() {

    vals := []int{-1, 0, 2, 1, 5, 4}

    ForEach(vals, func(e int, i int, data []int) {

        fmt.Printf("e at %d: %d\n", i, e)
    })

    fmt.Println("-------------------------")

    words := []string{"sky", "forest", "word", "cup", "coin"}

    ForEach(words, func(e string, i int, data []string) {

        fmt.Printf("e at %d: %s\n", i, e)
    })
}

The generic ForEach function takes a generic slice and a closure function as parameters. The closure is used to perform a task on each of the elements.

func ForEach[T any](data []T, f func(e T, i int, data []T)) {

    for i, e := range data {
        f(e, i, data)
    }
}

In the ForEach function, we use the for loop to go over the elements of the generic slice and call the closure on each element.

ForEach(vals, func(e int, i int, data []int) {

    fmt.Printf("e at %d: %d\n", i, e)
})

When actually calling the ForEach function, we pass a closure with concrete types. The elements have int type, the index has int type, and the collection is of int[] type.

$ go run main.go
e at 0: -1
e at 1: 0
e at 2: 2
e at 3: 1
e at 4: 5
e at 5: 4
-------------------------
e at 0: sky
e at 1: forest
e at 2: word
e at 3: cup
e at 4: coin

In this tutorial, we have covered generics in Golang.

List all Go tutorials.