Golang: Generics
Tags: go, 100daysofgolang
Published: 2023-07-25 12:56:47

## Introduction

In the 29th post of the series, we will be looking into generics in Golang. Generics were added in Golang version 1.18, so they are quite new in the world of Golang but the concept is quite old in other programming languages.

Generics provide a powerful toolset for writing more expressive and concise code that can handle a wide range of data types. With generics, we can write reusable algorithms, data structures, and functions that work seamlessly with various types, without sacrificing type safety.

We will learn how to create generic functions and work with generic types. Additionally, we will cover type constraints and interfaces, which allow us to specify requirements for the types used with our generics.

## Generic Type in Functions

We can define a generic type with the keyword `any` that is going to replace the type `T` i.e. any data type with the inferred type at compilation. This makes the code reusable to any relevant data type to be used for that operation/task.

We can provide the type `any` after the name of the function/struct to make it generic like `func Name[T any](x T)`. Here, the Name is a function that takes in a generic type `T` it could be any type and the variable is named as `x` that could be used inside the function.

We could also make the function take specific types instead of `any` but we will eventually move into that. However, for now, let's ease the process of learning and then move on to the optimizations and adding constraints.

```go
package main

import (
	"fmt"
)

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

func main() {
	Print("hello")
	Print(123)
	Print(3.148)
}
```


{% cta https://go.dev/play/p/t-ODmkHu5BJ %}Go Playground{% endcta %}

```bash
$ go run main.go
hello
123
3.148
```

The above is the simplest example that could be used to demonstrate a generic function. The function `Print` takes a parameter `stuff` of a generic type denoted by a type parameter `T`. The type parameter `T` serves as a placeholder that represents a specific type determined at compile time when the function is invoked.

This means, if in my code I do not call the function with the type `[]int` it won't have the function with the signature as `Print(stuff []int)` rather only the types which the function is called with are inferred and written with that specific types.

## Creating a Generic Slice

We can even create a slice with a generic type and allow any valid processing on the elements or the slice as a whole.

```go
package main

import (
	"fmt"
)

func ForEach[T any](arr []T, f func(T)) {
	for _, v := range arr {
		f(v)
	}
}

func main() {

	strSlice := []string{"b", "e", "a"}
	ForEach(strSlice, func(v string) {
		fmt.Println(v)
	})

	slice := []int{10, 25, 33, 42, 50}
	var evenSlice []int
	ForEach(slice, func(v int) {
		isEven := v%2 == 0
		if isEven {
			evenSlice = append(evenSlice, v)
		}
	})
	fmt.Println(evenSlice)

}
```

```bash
$ go run main.go

b
e
a
[10 42 50]
```

{% cta https://go.dev/play/p/tUwgbxnLc-1 %}Go Playground{% endcta %}

The `ForEach` is a generic function that iterates over a slice of any type and calls a function on each element. Let's break it down:

* `ForEach[T any]` declares this as a generic function that works on a slice of any type `T`.
    
* `arr []T` is the slice of elements we want to iterate over. It can be a slice of any type, ints, strings, `T` in general, etc. So it is a generic slice parameter.
    
* `f func(T)` is the function that will be called on each element. It takes a single parameter of type `T` which will be each element. So, this is a function parameter with a generic type as its parameter.
    

In the body, we range over the slice arr:

```go
for _, v := range arr {

}
```

On each iteration, `v` will be the next element. The underscore ignores the index. We call the provided function `f`, passing the element `v`: `f(v)`

So in summary:

* It allows iterating over a slice of any type
    
* This avoids having to know the specific slice type in the loop
    
* The provided function `f` is called on each element
    
* So it provides a reusable abstraction for processing slices generically.
    

Now, we will discuss the example used in the main function. First, we create a slice of strings as `strSlice := []string{"b", "e", "a"}`. Then we call the generic `ForEach` function, passing the string slice and a function to handle each element.

```bash
ForEach(strSlice, func(v string) {
  fmt.Println(v) 
})
```

Here, the `func(v string)` is the invocation of the `ForEach` function with the typed string and the variable name as v. The `v` is the element of the slice, so inside the function body(this is an anonymous function), we call the `fmt.Println(v)`, which will print each string in the slice.

```go
slice := []int{10, 25, 33, 42, 50}
var evenSlice []int
ForEach(slice, func(v int) {
    isEven := v%2 == 0
    if isEven {
        evenSlice = append(evenSlice, v)
    }
})
fmt.Println(evenSlice)
```

In the next example, we create a new slice of int as `slice := []int{10, 25, 33, 42, 50}`. Then we call the generic `ForEach` function again, passing the slice and a function to handle each element just as before, however, we are processing some things and then appending to an external slice.

First, the `slice := []int{10, 25, 33, 42, 50}` is created with some numbers, we also initialize another slice as `evenSlice := []int{}` which is empty. Then we call the generic `ForEach` function again, passing the slice and a function to handle each [element.Here](http://element.Here), the ForEach is called with the `slice` slice and not the `evenSlice` slice, so we are going to access each element in the `slice` array. We first create a `isEven` boolean that checks if the element is even or odd by `v%2 == 0`. This expression will result in `true` if `v` is even and `false` otherwise. So, only if the `isEven` is true, we append the element `v` into the `evenSlice` slice.

So, that's how generic slices can be handy for doing type-specific processing without writing functions for those individual types. This avoids needing to write type-specific functions for each slice type.

NOTE: Make sure to only use generic functions with generic slice types with the appropriate and valid conditions and use it only when it looks legible to do so.

## Creating a Generic Map

We can also create a generic map with the generic type of `map[K]T` where `K` is a generic type and `T` is the type of the key.

```go
package main

import (
	"fmt"
)

func GetValue[K comparable, V any](m map[K]V, key K, defaultVal V) V {
	if v, ok := m[key]; ok {
		return v
	}
	return defaultVal
}

func main() {

	serverStats := map[string]int{
		"port":      8000,
		"pings":     47,
		"status":    1,
		"endpoints": 13,
	}
	v := GetValue(serverStats, "status", -1)
	fmt.Println(v)
	v = GetValue(serverStats, "cpu", 4)
	fmt.Println(v)

}
```

```bash
$ go run main.go
1
4
```

{% cta https://go.dev/play/p/ludlh6UwKaD %}Go Playground{% endcta %}

GetValue is a generic function that takes three type parameters: The map itself, the key to find the value for, and a default value if the key doesn't exist.

The `m` is a map with keys of type K and values of type V, the key is of type K, and defaultVal is of type V. So, we have two generics here, as the key and value need not be of the same type, hence we have distinct generics here. K has added a constraint of `comparable` and `V` as `any` type. The type constraint comparable restricts K to be a comparable type, and the type constraint `any` allows V to be any type.

* Inside the function, we use the ok variable to check if the given `key` exists in the map `m`.
    
* If the key is present in the map (ok is true), we retrieve the corresponding value from the map and return it as `m[key]` which is stored in the variable `v`.
    
* If the key is not present in the map (ok is false), we return the provided `defaultVal`.
    

So, this is how we can use any type of map to retrieve the value of a key, the data type of key and value could be anything. It allows us to retrieve a value from a map irrespective of the pair type in the map.

NOTE: The type of `defaultVal` and the type of `v` should be the same since the map will need to have the value for the given key as the same type as defined in the map type(here `map[string]int` so `v` is `int` and so should be the `defaultVal`).

Moving into the main function, we create a map of `[string]int` i.e. the key is of type `string` and the value of type `int`. The map `serverStats` has a few keys like `port`, `pings`, `endpoints`, and `status`. We call the `GetValue` method on the map `serverStats` with the key `status` and provide a default value of `-1`. The function will readily return `1` since the key is present in the provided map. However, if we try to access the key `cpu`, the key is not present and the value is returned as the `defaultVal` which we passed as `4`.

So, this was a simple generic getter method on a map. We can get a value of a key in a map of any pair and even provide a default value if doesn't exist. However, it won't add it to the map, we will just return the value from the function that's it. We have to see that returned default value manually.

We can make another function to get or set the value of a key in a map. The function will take in a reference to the map rather than a copy of the map, we can then use that reference to set the key with the provided default value.

```go
package main

import (
    "fmt"
)

func GetOrSetValue[K comparable, V any](m *map[K]V, key K, defaultVal V) V {
    // reference the original map
	ref := *m
	if v, ok := ref[key]; ok {
		return v
	} else {
        //mutate the original map
        ref[key] = defaultVal

        return defaultVal
    }
}

func main() {
    serverStats := map[string]int{
        "port":      8000,
        "pings":     47,
        "status":    1,
        "endpoints": 13,
    }
    fmt.Println(serverStats)
    v := GetOrSetValue(&serverStats, "cpu", 4)
    fmt.Println(v)
    fmt.Println(serverStats)
}
```

```bash
$ go run main.go

map[endpoints:13 pings:47 port:8000 status:1]
4
map[cpu:4 endpoints:13 pings:47 port:8000 status:1]
```

{% cta https://go.dev/play/p/fYtjFQRaPCb %}Go Playground{% endcta %}

In the above code, we take a reference of the map as `*map[K]V`, this will give access to the actual place(memory address where the map is located so we could mutate/change it). The rest of the parameters are kept as is, the key will be taken as it was before, and so will the `defaultVal`. The only difference is that we will set the `key` doesn't exist, we set the `ref[key]` to the `defaultVal` and return the `defaultVal`.

For example, the `cpu` key is not present in the initial map `serverStats` so, when we call the `GetOrSetValue` with the reference to the map `serverStats`, key as `cpu` and the default value as `4`, the function returns `4` and the map is mutated with the key `cpu` with value `4`.

The takeaway is you can even use references to access the original data and mutate it based on your needs.

## Generic Type in Struct

We can define custom structs with generic type as the field values. The name of the struct is followed by the `[T any]` which is the type parameter to be used in the struct fields, again this type could have multiple types(since a struct can have many fields), not necessary want a single type parameter, you could have multiple type parameters bunched up just like we saw in the map example.

```go
package main

import (
	"fmt"
)

type Stack[T any] struct {
	items []T
}

func NewStack[T any]() *Stack[T] {
	return &Stack[T]{}
}

func (s *Stack[T]) Push(item T) {
	s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() T {
	if len(s.items) == 0 {
		panic("Stack is empty")
	}
	index := len(s.items) - 1
	item := s.items[index]
	s.items = s.items[:index]
	return item
}

func main() {
	intStack := NewStack[int]()
	intStack.Push(10)
	intStack.Push(20)
	intStack.Push(30)
	fmt.Println("Integer Stack")
	fmt.Println(intStack)
	intStack.Pop()
	intStack.Pop()
	fmt.Println(intStack)

	// without the NewStack method
	strStack := Stack[string]{}
	strStack.Push("c")
	strStack.Push("python")
	strStack.Push("mojo")
	fmt.Println("String Stack:")
	fmt.Println(strStack)
	strStack.Pop()
	fmt.Println(strStack)
}
```

```bash
$ go run main.go

Integer Stack
&{[10 20 30]}
&{[10]}

String Stack:
{[c python mojo]}
{[c python]}
```

{% cta https://go.dev/play/p/4t_P2mKtTZN %}Go Playground{% endcta %}

In this example, we have used the `Stack` example for doing a basic `Push` and `Pop` operation on the elements. Here the type of the underlying stack elements could be anything, hence the type parameter is defined for the `items` which is a list/slice of the type `T` as `[]T`. We have to specify the type before initializing the

We have created the `NewStack` method, it is not needed, it could be just used as `Stack[int]{}` to initialize a empty stack with int type(here `int` could be any other type you wish). I have just created it so that it shows the abstraction that could be built upon in real applications. The `NewStack` simply replaces the `T` with the provided `type` in the initialization.

The `Push` method is associated with the `Stack` struct, as we refer to the `*Stack[T]` indicating a reference to the Stack object with the type `T`. The method takes in the parameter `T` which would be the element to be added to the `Stack`. Since the function is tied to the Stack struct, we can simply mutate the underlying `items` by appending the provided value `item` in the parameter using `s.items = append(s.items, item)`. This appends the `item` to the underlying `items` list in the `Stack` object `s`

Similarly, we can create `Pop` method as well, which will first check if the `Stack` is not empty(i.e. the s.items slice has a length greater than 0), then get the index of the last element using `len(s.items) - 1` and then slice the `items` at index `[:last_index]`. This will basically get you all the elements except the last. Before we remove the element from the slice, we also store the item in `item` variable and return it from the method.

You could see the case of generics here, you could build complex data structures without creating a separate implementation for each type.

## Adding Constraints to Generics

We can add constraints to the generics to restrict the type of the generic parameter. For example, we can add a constraint for the generic type to be a slice of a specific type or we have seen in the map example the `comparable` constraint.

The `comparable` constraint is an interface that allows two instances of the same type to be compared i.e. support comparison operators like ==, &lt;, &gt;, !=, &gt;=, &lt;=, etc. It could be any numeric type like `int`, `float`, `uint` and variants, booleans, time duration, and any other struct that implements the `comparable` interface.

```go
package main

import (
	"fmt"
)

func FindIndex[T comparable](arr []T, value T) int {
	for i, v := range arr {
		if v == value {
			return i
		}
	}
	return -1
}

func main() {

	strSlice := []string{"m", "e", "e", "t"}
	fmt.Println(FindIndex(strSlice, "e"))
	fmt.Println(FindIndex(strSlice, "t"))
	fmt.Println(FindIndex(strSlice, "a"))

	intSlice := []int{10, 25, 33, 42, 50}
	fmt.Println(FindIndex(intSlice, 33))
	fmt.Println(FindIndex(intSlice, 90))

}
```

```bash
$ go run main.go
1
3
-1

2
-1
```

{% cta https://go.dev/play/p/fv9gzb8K4d7 %}Go Playground{% endcta %}

In the above example, we have created the function `FindIndex` that takes in a generic slice, the type parameter `[T comparable]` indicates that the type used for calling this method needs to have a type that implements the comparable interface (for the elements of the slice). The method takes in two parameters, one the slice as `[]T` and the other the value to find the index for as type `T`. The method returns a type `int` since the index of the slice has to be an integer value.

Inside the function body, we simply iterate over the slice `arr` and check if the element is equal to the provided value. If the element exists, we return that index, else we return `-1`

As we can see we have run a couple of slices with the function `FindIndex` with types `int` and `string`. The method returns an index value if the element exists, else it returns `-1`. The `comparable` is a built-in type constraint. We could even define custom constraints as interfaces that implement the types of the particular type(s).

Also, we could define custom constraints like numeric only, string only, etc.

```go
package main

import (
	"fmt"
)

type numeric interface {
	uint | uint8 | uint16 | uint32 | uint64 |
		int | int8 | int16 | int32 | int64 |
		float32 | float64
}

func Sum[T numeric](nums []T) T {
	var s T
	for _, n := range nums {
		s += n
	}
	return s
}

func main() {

	intSlice := []int{10, 20, 30, 40, 50}
	fmt.Println(Sum(intSlice))

	floatSlice := []float64{1.1, 2.2, 3.3, 4.4, 5.5}
	fmt.Println(Sum(floatSlice))
}
```

```bash
$ go run main.go

150
16.5
```

{% cta https://go.dev/play/p/_1eGl58qQ-2 %}Go Playground{% endcta %}

In the above example, we have created the `numeric` constraints that allow the type `int`, `float` and their variants to be allowed in the numeric generic type. The function `Sum` is a generic function with the constraint of `numeric` type parameter. The method takes in the parameter as type `[]T` and returns the type as `T`. The method will simply iterate over the slice and return the sum of its elements.

This will allow any numeric type which can be added and the sum can be obtained, so if we try to call the method with other types like `string` or `maps`, it won't work, and give an error:

```bash
$ go run constraints.go

# command-line-arguments                                                                                                               
scripts/generics/constraints.go:46:20: 
string does not satisfy numeric (string missing in uint | uint8 | uint16 | uint32 | uint64 | int
 | int8 | int16 | int32 | int64 | float32 | float64)

shell returned 1
```

So, we can use the constraint to restrict the type of the generic type parameter which will allow us to restrict the usage and avoid any unsafe type to be used in the generic function.

Also, if we have a custom type with the base types, we need to use `~` before the type to accept it into the generic constraint. This will allow any approximate type to be allowed in the constraint. Let's say we are implementing a custom string type, for that to work with a constraint, it won't be satisfied in the constraint since its type is `CustomString` and not `string`. So to avoid this we use `~string` that would approximate the type and allow abstracted base types.

```go
package main

import (
	"fmt"
)

type string2 string

type strings interface {
	~string
}

func PrintEach[T strings](arr T) {
	for _, v := range arr {
		fmt.Printf("%c\n", v)
	}
}

func main() {
	var s string2
	s = "hello"
	PrintEach(s)

}
```

```bash
$ go run main.go

h
e
l
l
o
```

{% cta https://go.dev/play/p/N-66A9C94ps %}Go Playground{% endcta %}

In the above example, we have used the type approximations in the type constraint `strings`, it allows any string type, whether a base `string` type or an abstract `string` type. If you try to remove the `~` in `~string`, it will result in the error that `string2 does not satisfy strings` interface. So, by adding `~` to the `string` type the abstract type `string2` can be satisfied in the generic constraint.

That's it from the 29th part of the series, all the source code for the examples are linked in the GitHub on the [100 days of Golang](https://github.com/Mr-Destructive/100-days-of-golang/tree/main/scripts/generics) repository.

{% embed https://github.com/Mr-Destructive/100-days-of-golang/ %}

### References

- [Generics in Go](https://bitfieldconsulting.com/golang/generics)
- [Understanding generics in Go](https://blog.logrocket.com/understanding-generics-go-1-18/)

## Conclusion

From this section of the series, we have covered the basics of generics in Golang. By using generics in functions, slices, maps, and structs, and adding constraints to them the fundamental usage of generics was covered.

If you have any questions, feedback, or suggestions, please drop them in the comments/social handles or discuss them below. Thank you so much for reading. Happy Coding :)