Introduction to Generics in Go

27 Nov 2022
Introduction to Generics in Go

Given that Go is a typed language, you provide data types whenever you declare variables, functional arguments and return types. With generics, you can declare and use functions or types that are written to work with any of a set of types provided by calling code. Before generics you could use interface or another common pattern was using map[string]interface to support dynamic inputs (example in JSON parsing).

Generics help in forcing static typing that gets missed when using interface. You need to maintain type conversions and handle errors related to it. It allows to reuse certain logic of code for different types of inputs.


Solving the non-generic way

Prerequisite: Installation of Go 1.18 or later. You can check current version using go version command.

Let’s get the setup done quickly so that we can deep dive into making our code generic.

mkdir learn_generics
cd learn_generics
go mod init learn_generics
mkdir generics
touch main.go

Now open your favourite editor and let’s add simple functionality of reversing a string array. Let’s create a reverse.go inside our generics directory and add following code.

// non_generic.go
package generics

func ReverseString(input []string) []string {
	for i, j := 0, len(input)-1; i < j; i, j = i+1, j-1 {
		input[i], input[j] = input[j], input[i]
	}
	return input
}

We’ll run this from our main to verify if it’s working as expected.

// main.go
package main

import (
	"blog_codes/generics"
	"fmt"
)

func main() {
  inputStringArr := []string{"a", "b", "c", "d", "e"}
	outputStringArr := generics.ReverseString(inputStringArr)
	fmt.Println("Reverse string array: ", outputStringArr)
}

From you command line run go run main.go to execute our code.

$ go run main.go
[e d c b a]

Simple enough, right? Let’s also add functionality to reverse integer arrays?

// non_generic.go

// earlier code ...

func ReverseInteger(input []int) []int {
	for i, j := 0, len(input)-1; i < j; i, j = i+1, j-1 {
		input[i], input[j] = input[j], input[i]
	}
	return input
}

We’ll also update main.go to get this runnning.

package main

import (
	"blog_codes/generics"
	"fmt"
)

func main() {
	inputStringArr := []string{"a", "b", "c", "d", "e"}
	outputStringArr := generics.ReverseString(inputStringArr)
	fmt.Println("Reverse string array: ", outputStringArr)

	inputIntArr := []int{1, 2, 3, 4, 5}
	outputIntArr := generics.ReverseInteger(inputIntArr)
	fmt.Println("Reverse integer array: ", outputIntArr)
}

The output we now get

$ go run main.go
Reverse string array:  [e d c b a]
Reverse integer array:  [5 4 3 2 1]

Ok, we did some basic implementation for reversing an array. What’s the point? There are some key points to notice here:

  • Can we use same function for both data type? - No, input and type is string struct vs int struct. See that’s why we called it non-generic.
  • Is there a similarity you see in both reverse functions? - Yes, logic for both implementations are actually same. We’re iterating over array and swapping values to reverse it.
  • Can we write a common reverse function that works out not just for string or integer rather other data types as well?

Yes, now we can do this quite easily in Go using generics.


Solving the Generic way

We’ll create a generic.go file in our generics directory and update it with a generic reverse method. Don’t worry if you don’t get it at first look. We’ll go through the code together.

// generic.go
package generics

func ReverseGeneric[T any](input []T) []T {
	l := len(input)
	output := make([]T, l)

	for i, ele := range input {
		output[l-i-1] = ele
	}
	return output
}

We’ll use the same arrays but now use our generic reverse function.

// main.go
package main

import (
	"blog_codes/generics"
	"fmt"
)

func main() {
	inputStringArr := []string{"a", "b", "c", "d", "e"}
	inputIntArr := []int{1, 2, 3, 4, 5}
	
	genericOutput := generics.ReverseGeneric(inputStringArr)
	fmt.Println("Reversed string array (generic way): ", genericOutput)
	
  genericIntOutput := generics.ReverseGeneric(inputIntArr)
	fmt.Println("Reversed integer array (generic way): ", genericIntOutput)
}

On running now, you’ll see the following output.

Reversed string array (generic way):  [a b c d e]
Reversed integer array (generic way):  [1 2 3 4 5]

Yay :tada: You just wrote your first generic code. Time to deep dive on what’s happening here.

  • [T any] - In our function declaration we added this. These are abstract data types that we call type parameters which help in making our functions generic and allowing it to work with arguments of different types. The type parameters are added withing square brackets. That’s how go knows that these are type params. Here we’ve use any as our type that means our function can support any data type inputs. any is an alias for interface{} which basically allows any type.

  • [s []T] - Here we define input params to our function which says that I can accept array of type T (which is any as defined above).

  • output := make([]T, l) - The logic is similar only difference here is that we’re creating a new struct of input type T and reversing the input array. This is same as writing output := make([]int, l) for integer array.

  • Note: Type parameter you’ve define must support all the operations the generic code is performing on it.

  • There are certain predefined constraints like comparable in Go. It only allows data types whose values may be used as an operand of the comparison operators == and !=. Instead of allowing any data type this restricts on what input it can support. These type contraints are essentially interface types.

  • Question: should we use T comparable or T any ? Or both will work? I’ll leave this as an exercise for you.

  • You might wonder but any , comparable etc would just cover certain cases. What if i want to control what types to allow. Generics provides a way to solve that as well. Continuing our example, we could have created an interface and used that instead.

    type MyDataTypes interface {
        string | int | int64
    }
    
    func ReverseGeneric[T MyDataType](input []T) []T {
    	l := len(input)
    	output := make([]T, l)
    
    	for i, ele := range input {
    		output[l-i-1] = ele
    	}
    	return output
    }
    

Should I use Generics?

Well, to be honest generics in go is still quite new. Though I personally love how easy it is to use but for a lot of cases using interface with implementations might be the easier way as the code starts getting more complex. Don’t think of generics as something that will magically make your life 10x better. It’s just another way to solve a problem. In all the prod codebases that I’ve worked related to Go, I’m yet to see generics code being used. Hopefully soon 🤞


Resources

Books to learn Golang

Liked the article? Consider supporting me ☕️


I hope you learned something new. Feel free to suggest improvements ✔️

Follow me on Twitter for updates and resources. Let’s connect!

Keep exploring 🔎 Keep learning 🚀

Liked the content? Do support :)

Paypal - Mohit Khare
Buy me a coffee