DEV Community

Vivek Alhat
Vivek Alhat

Posted on

Demystifying Go Generics

Go 1.18 introduced support for generics. Generics allow you to write code that can work with any set of types. With generics, you can write functions or types that are independent of specific types.

Functions: From Specific to Generic

Let's begin with a simple adder function. We will first implement it without using generics.

package main

import "fmt"

func adder(nums []int64) int64 {
    var sum int64
    for _, v := range nums {
        sum += v
    }
    return sum
}

func main() {
    nums := []int64{1, 2, 3, 4, 5}
    fmt.Println(adder(nums))
}
Enter fullscreen mode Exit fullscreen mode

In the code snippet above, we have a simple adder function that takes a slice of int64 type and returns the sum of the int64 values. While this function works fine for int64, it would need to be rewritten to support a different type, such as float64, if we want to compute the sum of an array of float64 values.

package main

import "fmt"

func adderInt(nums []int64) int64 {
    var sum int64
    for _, v := range nums {
        sum += v
    }
    return sum
}

func adderFloat(nums []float64) float64 {
    var sum float64
    for _, v := range nums {
        sum += v
    }
    return sum
}

func main() {
    ints := []int64{1, 2, 3, 4, 5}
    fmt.Println(adderInt(ints))

    floats := []float64{1.1, 2.2, 3.3, 4.4, 5.5}
    fmt.Println(adderFloat(floats))
}
Enter fullscreen mode Exit fullscreen mode

This results in code duplication. Generics provide a way to write functions that can work with a variety of types, reducing redundancy in the code.

Now, let's write the same adder function using generics.

package main

import "fmt"

func adder[T int64 | float64](nums []T) T {
    var sum T
    for _, v := range nums {
        sum += v
    }
    return sum
}

func main() {
    ints := []int64{1, 2, 3, 4, 5}
    fmt.Println(adder(ints))

    floats := []float64{1.1, 2.2, 3.3, 4.4, 5.5}
    fmt.Println(adder(floats))
}
Enter fullscreen mode Exit fullscreen mode

In the provided code snippet above, we have effectively addressed the issue of code redundancy by introducing a versatile adder function that is capable of supporting different data types such as int64 and float64.

It is worth emphasizing the significant advantages that generics bring to the table when it comes to enhancing the overall readability of the code. By using generics, developers can write more concise and expressive code, making it easier for others to understand and maintain the code in the long run.

Syntax

func adder[T int64 | float64](nums []T) T {}
Enter fullscreen mode Exit fullscreen mode

The syntax for creating a generic function is straightforward. The function name is followed by square brackets, where you can define a type parameter (T) for the function. This type parameter is then applied to denote both the function's parameters and its return type.

Consider the example of the "adder" function mentioned earlier; it's designed to accommodate both int64 and float64 types seamlessly. However, attempting to use any other type will result in an error.

Structs: Embracing Type Flexibility

In addition to generic functions, it's possible to create generic structs. Below is an example of a generic user struct.

package main

import "fmt"

type User[T int64 | string] struct {
    id   T
    name string
}

func main() {
    userA := User[int64]{id: 1, name: "John Doe"}
    userB := User[string]{id: "A1", name: "Jane Doe"}
    fmt.Printf("%+v, %+v\n", userA, userB)
}
Enter fullscreen mode Exit fullscreen mode

In generic structs, the structure's name is succeeded by square brackets, providing a space to define a type parameter. When instantiating a generic struct, we have to specify the desired type by passing it in the square brackets. This ensures that the struct operates with the intended data type offering flexibility.

For example:

    userA := User[int64]{id: 1, name: "John Doe"} // T is int64
    userB := User[string]{id: "A1", name: "Jane Doe"} // T is string
Enter fullscreen mode Exit fullscreen mode

Map: Generic Data Structures

Map, a fundamental data structure, can also benefit from generics.

package main

import "fmt"

type Number interface {
    int64 | float64
}

type GenericMap[K comparable, V Number] map[K]V

func main() {
    m := make(GenericMap[string, int64])
    m["A"] = 1
    m["B"] = 2

    fmt.Printf("%+v\n", m)
}
Enter fullscreen mode Exit fullscreen mode

In the code snippet above, we have created a custom generic map type where the key implements a comparable type constraint and the value implements a Number interface that supports int64 and float64 types.

You can read more about comparable type constraint on the Go blog.

Constraints

The Go constraints package enhances type safety by simplifying type declarations. Instead of explicitly listing acceptable types, constraints allow a more succinct approach.

package main

import (
    "fmt"

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

func adder[T constraints.Ordered](nums []T) T {
    var sum T
    for _, v := range nums {
        sum += v
    }
    return sum
}

func main() {
    ints := []int64{1, 2, 3, 4, 5}
    fmt.Println(adder(ints))

    floats := []float64{1.1, 2.2, 3.3, 4.4, 5.5}
    fmt.Println(adder(floats))
}
Enter fullscreen mode Exit fullscreen mode

Here, constraints.Ordered serves as a catch-all for ordered types, eliminating the need for exhaustive type unions.

You can read more about different constraints here.

Top comments (0)