Generics

blog-image

Let’s look at the hottest feature in Go 1.18

Estimated Reading Time : 10m

Undestanding the value of generics

Consider the following code:


    func addInt(a, b int) int {
        return a + b
    }

    func addFloat64(a, b float64) float64 {
        return a + b
    }

We have two separate functions for adding two separate numeric types.

Starting with Go 1.18 we can simplify this by changing the code to read:


    func addIntOrFloat64[T int | float64] (a, b T) T {
        return a + b
    }

This example shows us how generics can help reduce the amount of code we need to write.

Generic Type Constraints

Our previous example only accounts for 2 numeric types.

Let’s say we want to account for different size ints and floats.

We can do so by creating a custon num type that encompasses any numeric types we want to support:


    type num interface {
        int8 | int16 | int | int64 | float32 | float64
    }

    func addNum[T num](a, b T) T {
        return a + b
    }

Pre-defined Generic types

By importing golang.org/x/exp/constraints, we can leverage several pre-defined generic type constraints for our code.

We can improve our code by using constraints.Ordered.

Not only does this simplify the code, but it also expands it by including both signed and unsigned ints as well strings and string alias types:


    package main

    import (
        "fmt"

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

    func add[T constraints.Ordered](a, b T) T {
        return a + b
    }

    func main() {
        fmt.Printf("signed numberics result: %v\n", Add(-1, -2.3))
        fmt.Printf("unsigned numerics result: %v\n", Add(1, 2.3))
        fmt.Printf("string result: %v\n", Add("go", "1.18"))

        type myStringAlias string
        var s myStringAlias = "i'm special"
        fmt.Printf("string alias result: %v\n", Add(s, "🙂"))
    }

Output:


    $go run main.go
    signed numberics result: -3.3
    unsigned numerics result: 3.3
    string result: go1.18
    string alias result: i'm special🙂

Explicitly Supporting Type-Aliases

The previous example adds type-alias support under the hood.

We’ll use a different example now to understand how to explicitly add type-alias support.

When adding explicit-type alias result, you’ll need to add a ~ symbol in front of the type you’re aliasing in the function signature like so:



    package main

    import (
        "fmt"
    )

    type specialInt int

    func add[T ~int | float64](a, b T) T {
        return a + b
    }

    func main() {
        var a, b specialInt = 2, 3
        fmt.Printf("int alias result: %v\n", add(a, b))
    }

Generic Struct Fields

We can also use generics when defining and initializing structs.

Just be aware that structs initialized with different param types are themselves not the same type.


    package main

    import (
        "fmt"

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

    type myStruct[T constraints.Ordered] struct {
        field T
    }

    func main() {
        struct1 := myStruct[string]{"hello there"}
        fmt.Printf("struct1 field value: %v\n", struct1.field)
        struct2 := myStruct[int]{1}
        fmt.Printf("struct2 field value: %v\n", struct2.field)

        fmt.Printf("type of struct1: %T\n", struct1)
        fmt.Printf("type of struct2: %T\n", struct2)

    }

Just be aware that struct1 and struct2 are different types in this context.


go run main.go
struct1 field value: hello there
struct2 field value: 1
type of struct1: main.myStruct[string]
type of struct2: main.myStruct[int]

The comparable key word

It’s a new key word that represents an interface type that is implemented by any type that can be compared with equality check operators like ==, !=.

Lets' look at a couple function definition from the standard library that use this.

slices.Contains


    // Contains reports whether v is present in s.
    func Contains[S ~[]E, E comparable](s S, v E) bool {
        return Index(s, v) >= 0
    }

The first param is a slice of any type that can be compared as well as any aliases of that type.

The second param is the target type.

slices.Index

Index simply returns a non -1 value if the presense of the target value is detected.


    // Index returns the index of the first occurrence of v in s,
    // or -1 if not present.
    func Index[S ~[]E, E comparable](s S, v E) int {
        for i := range s {
            if v == s[i] {
                return i
            }
        }
        return -1
    }

Let’s see this in action:


    package main

    import (
        "fmt"
        "slices"
    )

    func main() {
        slice1 := []string{"a", "b", "c"}
        containsC := slices.Contains[[]string](slice1, "c")
        fmt.Printf("contains c? %t\n", containsC)

        slice2 := []int{1, 2, 3}
        contains2 := slices.Contains[[]int](slice2, 2)
        fmt.Printf("contains 2? %t\n", contains2)
    }


$go run example1/main.go
contains c? true
contains 2? true

Note: slices package was introduced in Go 1.21

Conclusion

Generics is a powerful feature that expands how flexible the language is and also helps us reduce how much code we need to write.

A great weapon to add to any Go ninja’s arsenal.

Much love,

-Faris