DEV Community

Cover image for Trying Out Go v1.18 Generics
Mohamad Harith
Mohamad Harith

Posted on

Trying Out Go v1.18 Generics

#go

Generics allow us to write functions that can be used with arguments of any data type. The latest version of Go has introduced generics into the language. Therefore, in this article I'd like to explore the usage of generics in Go.

Before Go v1.18

The were no generics in the older versions of Go. However one workaround for generics in older versions of Go is the empty interface, interface{} in combination with type assertion and reflection. For example, lets say we want to write a function that would take an array of any type and sum up the array, in older versions of Go we would have to do something like this:

func sumAnyType(i interface{}) (o interface{}) {

    value := reflect.ValueOf(i)

    if reflect.TypeOf(i).Kind() != reflect.Slice || value.Len() < 1 {
        return
    }

    switch value.Index(0).Kind() {
    case reflect.Int:
        var res int = 0
        for i := 0; i < value.Len(); i++ {
            res += value.Index(i).Interface().(int)
        }
        return res
    case reflect.Float64:
        var res float64 = 0
        for i := 0; i < value.Len(); i++ {
            res += value.Index(i).Interface().(float64)
        }
        return res
    }

    return
}

func main() {

    log.Println(sumAnyType([]int{1, 2, 3})) // prints 6

    log.Println(sumAnyType([]float64{1.2, 2.5, 3.9})) // prints 7.6

    log.Println(sumAnyType([]float32{1.2, 2.5, 3.9})) // prints nil
}
Enter fullscreen mode Exit fullscreen mode

Although the above works, there are some downsides one of which is code repetition. We are writing the summing logic for each data types and if the logic for particular data type is not written, the array of that type cant be summed.

Go v1.18 Generics

With the introduction of generics in the latest version of Go, the above example can be simplified to the following:

func sumAnyType[T int | float64](i []T) (o T) {
    for _, v := range i {
        o += v
    }
    return
}

func main() {

    log.Println(sumAnyType([]int{1, 2, 3})) // prints 6

    log.Println(sumAnyType([]float64{1.2, 2.5, 3.9})) // prints 7.6

    // log.Println(sumAnyType([]float32{1.2, 2.5, 3.9})) // does not compile
}
Enter fullscreen mode Exit fullscreen mode

As you see above, Go generics greatly simplifies the codes and enforces the Don't Repeat Yourself (DRY) principle as we are able to use the same function for multiple data types without having to rewrite the same logic for each data types.

Now that we understand the significance of generics in Go, lets understand the various features introduced for generics in Go v1.18:

1. The any keyword
The any keyword is just an alias to the empty interface, interface{}. That means, anything that we can do with interface{} can also be done with any. For example:

func printType(i any) {
    _, ok := i.(string)
    if ok {
        log.Println("its a string")
    }
    _, ok = i.(int)
    if ok {
        log.Println("its a integer")
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Type parameters, type arguments and type constraints
Type parameters is what allows a function to be generic, allowing it to work with arguments of different types. We'll call the function with type arguments and the ordinary function arguments. Type constraint defines a list of types a function can receive.
Image description

3. The comparable type costraint
comparable is a predeclared type constraint in Go v1.18 that denotes the types that can be compared using == or !=.

4. Type constraint as an interface
The type constraint can also be declared as an interface so that it can be reused. For example:


type CustomConstraint interface {
    int | float64
}

func addOne[T CustomConstraint](i T) (o T) {
    o = i + 1
    return
}

func multiply2[T CustomConstraint](i T) (o T) {
    o = i * 2
    return
}

func main() {

    log.Println(addOne[int](1))
    log.Println(multiply2[float64](3.14))
}
Enter fullscreen mode Exit fullscreen mode

5. The ~ keyword

A type constraint can be prefixed with ~ to restrict all the custom types whose underlying type is the same as the constraint.

The following does not compile because CustomConstraint only restrict type int:

type CustomInt int

type CustomConstraint interface {
    int
}

func addOne[T CustomConstraint](i T) (o T) {
    o = i + 1
    return
}

func main() {

    log.Println(addOne[CustomInt](1))
}
Enter fullscreen mode Exit fullscreen mode

The following compiles successfully because CustomConstraint restricts int and all the other custom types whose underlying type is int:

type CustomInt int

type CustomConstraint interface {
    ~int
}

func addOne[T CustomConstraint](i T) (o T) {
    o = i + 1
    return
}

func main() {

    log.Println(addOne[CustomInt](1))
}
Enter fullscreen mode Exit fullscreen mode

As a conclusion, Go generics is really useful especially to enforce DRY and can be used for various use cases. In my opinion, generics in Go is simpler and easier to understand compared to other languages such as Java.

Additional resources:

Discussion (0)