Getting started with Golang Generics

Introduction to Golang Generics

Go, unlike other programming languages like Python, is very strict when it comes to working with different types of data. In Python for example, a function that takes two numbers and  returns the maximum number, does not really need to strictly specify the type of the parameters like float or integer. The same function in Go would require that we define two functions that take two parameters of the same type float or int and return the maximum number as with float or int.

Considering the above statement, generics in Go allows us to declare and use functions or types that can work with any set of types.

Advertisement

 

Prerequisite

  1. Install Go version 1.18 or later. Generics is new and it does not exist in older Go versions below 1.18.
  2. A code editor, I recommend VS code, but other editors will work just fine.

In this article, we will learn about Go generics and how to implement generics. We will cover.

  1. Non-Generic functions
  2. Generic functions
  3. Generic type constraints

 

Why Golang Generics can be useful?

Currently, Go supports multiple data types in functions such as fmt.Println() using the empty interface and reflection. However, demanding every programmer to write lots of code and implement lots of functions and methods for supporting multiple custom data types is not the optimal solution—generics comes into play for providing an alternative to the use of interfaces and reflection for supporting multiple data types. The following code showcases how and where generics can be useful:

package main
import (
    "fmt"
)
func Print[T any](s []T) {
    for _, v := range s {
        fmt.Print(v, " ")
    }
    fmt.Println()
}
func main() {
    Ints := []int{1, 2, 3}
    Strings := []string{"One", "Two", "Three"}
    Print(Ints)
    Print(Strings)
}

In this case, we have a function named Print() that uses generics through a generics variable, which is specified by the use of [T any] after the function name and before the function parameters. Due to the use of [T any]Print() can accept any slice of any data type and work with it. However, Print() does not work with input other than slices and that is fine because if your application supports slices of different data types, this function can still save you from having to implement multiple functions for supporting each distinct slice. This is the general idea behind generics.

 

Set up Lab Environment

Create a working directory in your workspace called generics , and add a main.go file in the generics folder. Next step is to initialize a go module. The following steps work for Windows, Linux and Mac OS.

$ mkdir generics
$ cd generics
$ touch main.go
$ go mod init example/generics

 

Non-Generic function

Consider two functions that return the sum of all numbers in a number array. This requires that we create two separate functions at least, one will return the sum in float64 and the other in int64. This means we will have double work.

Example

Advertisement
package main
 
import "fmt"
 
func main() {
   f := []float64{1.0, 2.0, 3.0, 4.0, 5.0}
   i := []int64{1, 2, 3, 4, 5}
 
   s1 := SumOfFloat(f)
   s2 := SumOfIntegers(i)
 
   fmt.Println("Sum for float64 :", s1)
   fmt.Println("Sum for int64 :", s2)
 
}
 
func SumOfFloat(nums []float64) float64 {
   var sum float64
 
   for _, num := range nums {
       sum += num
   }
 
   return sum
}
 
func SumOfIntegers(nums []int64) int64 {
   var sum int64
 
   for _, num := range nums {
       sum += num
   }
 
   return sum
}

Output

$ go run main.go
Sum for float64 : 15
Sum for int64 : 15

Explanation

In the above example, we define two functions that work with arrays of type float64 and int64. These two functions return the sum of all numbers in their respective number arrays.

This is double work, these functions can be generalized/ merged into one function hence saving us time and effort.

 

Golang Generic functions

In this section we will create a single function that pretty much takes an array of numbers i.e float64 or int64 and returns the sum of the numbers in the array. This will effectively replace the sumOfFloat() and sumOfInteger() functions.

To be able to define a function that accepts both float64 and int64 , our new generic function will need to declare what types it supports. The calling code is required  determine whether it is calling a float64 or an int64. To support these new features, our new function signature will change  a little bit. It will need to declare type parameters in addition to its ordinary parameter. This will effectively convert our function to a generic function hence enabling it to work with different types.

Each parameter will have a type constraint which specifies the permissible type arguments that the calling code can use for the respective type parameters.At compile time, these parameters will represent a single type, the type provided as a type argument by the calling code.

It is worth mentioning that a type parameter must support all the operations that the generic function performs. For example, a generic function that works on both string and number parameters, and you try to index both string and number, this will not work because indexing does not work with number types i.e int64, float64 and this will cause the code not to compile.

Syntax

func genericFunction[T any](s []T) []T{

}

Explanation

In the above syntax example, the<b> []</b> after the function name are used to specify type parameters, which is a list of identifiers and a constraint interface. The <b>T</b> here is a type parameter used to define arguments and return the type of the function.

The parameter is also accessible inside the function. <b>any</b> is an interface and <b>T</b> has to implement this interface.  Go version 1.18 introduced <b>any</b> to basically refer to an empty interface i.e interface{}

Advertisement

Example

package main
 
import "fmt"
 
func main() {
   f := []float64{1.0, 2.0, 3.0, 4.0, 5.0}
   i := []int64{1, 2, 3, 4, 5}
 
   s1 := genericSum(f)
   s2 := genericSum(i)
 
   fmt.Println("Sum for float64 :", s1)
   fmt.Println("Sum for int64 :", s2)
 
}
 
func genericSum[N int64 | float64](nums []N) N {
   var sum N
 
   for _, num := range nums {
       sum += num
   }
 
   return sum
}

Output

$ go run main.go
Sum for float64 : 15
Sum for int64 : 15

Explanation

In the above example , we demonstrate how to implement a generic function called genericSum() . genericSum() function defines an array of parameter types inside the [] brackets. The first value N in the [] brackets defines the argument in the function call and also it represents the return value. N is basically the new type that has been created after creating a union of two types. In our case N is a union of float64 and int64.

In the function argument, nums represent an array of N type (union of float64 and int64) as demonstrated by []N. The return type for the genericSum() function uses the N type.

In the genericSum() function body, we declare a variable of type N. We then number through an array of nums(N[]) and calculate the sum and finally return the sum.

Advertisement

 

Generic type constraints

Assume you have a generic function that has many type parameters, say int, int8 , int26, int32, int64, float32, float64. Our function signature will be very long and undesirable to the eye. It is possible to put all these type parameters into one type using an interface.Essentially , we are moving the union from the function declaration into a new type constraint.

Example

package main
 
import "fmt"
 
type Number interface {
   int64 | float64
}
 
func main() {
   f := []float64{1.0, 2.0, 3.0, 4.0, 5.0}
   i := []int64{1, 2, 3, 4, 5}
 
   s1 := genericSum(f)
   s2 := genericSum(i)
 
   fmt.Println("Sum for float64 :", s1)
   fmt.Println("Sum for int64 :", s2)
 
}
 
func genericSum[N Number](nums []N) N {
   var sum N
 
   for _, num := range nums {
       sum += num
   }
 
   return sum
}

Output

$ go run main.go
Sum for float64 : 15
Sum for int64 : 15

Explanation

In the above example, we add a new type called <b>Number</b>, which is an interface type. This Number interface type now holds the union of type float64 and int64.The genericSum() function signature changes and instead of using literal union of float64 and int64, it uses the Number interface instead. The rest of the function remains as it were.

 

Summary

This article is about Go generics and can be used by both beginners and seasoned Go developers , because generics is the new kid in the block. We have learned about functions with and without generic and it is clear that generics save us time and effort. Generics is not a replacement for interfaces. These two have been designed to work together to make Go type safe, clean by preventing repetition.

Advertisement

We end this section by stating some useful facts about golang generics:

  • You do not need to use generics in your programs all the time.
  • You can continue working with Go as before even if you use generics.
  • You can fully replace generics code with non-generics code. The question is are you willing to write the extra code required for this?
  • I believe that generics should be used when they can create simpler code and designs. It is better to have repetitive straightforward code than optimal abstractions that slow down your applications.
  • There are times that you need to limit the data types that are supported by a function that uses generics—this is not a bad thing as all data types do not share the same capabilities. Generally speaking, generics can be useful when processing data types that share some characteristics.

 

References

https://go.dev/doc/tutorial/generics

 

Categories GO

Didn't find what you were looking for? Perform a quick search across GoLinuxCloud

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can either use the comments section or contact me form.

Thank You for your support!!

Leave a Comment

X