Generics

One of the significant limitations encountered in Go prior to version 1.18 is the inability to create functions that can operate seamlessly across multiple input and output data types. This restriction undermines the language's capacity to facilitate code abstraction and maintainability, as developers are compelled to duplicate function implementations for each distinct data type they wish to handle. Consequently, this not only introduces redundancy and increases the likelihood of errors but also inhibits the creation of generic solutions that can adapt to varying data structures. As a result, the absence of generics in Go prior to version 1.18 imposes considerable constraints on developers, impeding their ability to write concise, efficient, and adaptable code.

Golang generics

Let's start with the code below to illustrate why we need generics. If I want to create a function for adding numbers and concatenating strings, even though the operator in each of them is just '+', I cannot use one function for both. A larger issue is that even if I want to add two floating-point numbers, I need to write another function.

package main

import "fmt"

func main() {
	var n_1, n_2 int
	fmt.Println(AddNumbers(n_1, n_2))

	var f_1, f_2 float64
	fmt.Println(AddFloats(f_1, f_2))

	var s_1, s_2 string
	fmt.Println(AddStrings(s_1, s_2))
}

func AddNumbers(first, second int) int {
	return first + second
}

func AddFloats(first, second float64) float64 {
	return first + second
}

func AddStrings(first, second string) string {
	return first + second
}

If you want to create a function that can be used for all these types to sum them, we can use generics like the example below:

package main

import "fmt"

func main() {
	var n_1, n_2 int
	fmt.Println(Add(n_1, n_2))

	var f_1, f_2 float64
	fmt.Println(Add(f_1, f_2))

	var s_1, s_2 string
	fmt.Println(Add(s_1, s_2))
}

func Add[T int | float64 | string](first, second T) T {
	return first + second
}

Or

package main

import "fmt"

func main() {
	// You can omit [int] after the name because the type can be inferred from the input.
	Print[int]([]int{1, 2, 3, 4, 5})

	Print([]string{
		"hello",
		"bye",
	})
}

func Print[T any](slice []T) {
	for _, value := range slice {
		fmt.Printf("%v\n", value)
	}
}

After the function name, we should introduce a new type (here T) and specify the types that can be used for T. We can create multiple generic types if needed.

If the number of supported types becomes extensive and it becomes unwieldy to list them all on one line, we can use an interface to represent those types.

package main

func main() {

	// And we can utilize any of these types as both input and output for this function.
	/*
	 * int
	 * uint
	 * int8
	 * uint8
	 * int16
	 * uint16
	 * int32
	 * uint32
	 * int64
	 * uint64
	 * float32
	 * float64
	 * string
	 */
}

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

func Add[T Addable](first, second T) T {
	return first + second
}

Also, we can employ generics in types besides functions:

package main

type List[T any] struct {
	next  *List[T]
	value T
}

func NewList[T any]() *List[T] {
	return &List[T]{}
}

func main() {
	l := NewList[string]()
}

Here, any signifies any type from built-in or custom types that you can use for the type.

You may encounter ~ preceding each type in some interfaces, as demonstrated below:

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

What does that mean? If you're familiar with Golang, you may know that we can declare a new type from another type like this:

package main

import "fmt"

// declare a new type from int
type X int

type Int_1 interface {
	int
}

type Int_2 interface {
	~int
}

func CheckAccept1[T Int_1](value T) {
	fmt.Printf("accept %v\n", value)
}

func CheckAccept2[T Int_2](value T) {
	fmt.Printf("accept %v\n", value)
}

func main() {
	var y X

	// Error - X does not satisfy Int_1
	CheckAccept1[X](y)

	// Ok
	CheckAccept2[X](y)
}

It means that if you use ~ before a type in generics, you can use that type and other types declared based on that type. However, without it, you will encounter an error.

It's all about generics. Now you know everything related to generics in Golang. Contrary to their name, generics are quite simple and don't require much explanation.

Thanks for reading.😀