Demystifying Go Generics
Tags: programming, beginners, go
Published: 2024-02-03 18:24:28

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.

```go
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))
}
```

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.

```go
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))
}
```

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.

```go
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))
}
```

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

```go
func adder[T int64 | float64](nums []T) T {}
```

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.

```go
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)
}
```

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:

```go
	userA := User[int64]{id: 1, name: "John Doe"} // T is int64
	userB := User[string]{id: "A1", name: "Jane Doe"} // T is string
```

## Map: Generic Data Structures

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

```go
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)
}
```

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](https://go.dev/blog/comparable).

## Constraints

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

```go
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))
}
```

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](https://pkg.go.dev/golang.org/x/exp/constraints).