Getting Started with Go Generics: A Quick Overview

PRATHEESH PC
6 min readApr 7, 2023

Let’s start with a fundamental grasp of how to utilize Golang generic with some of the data types.

Generics are a powerful feature in programming languages that allow programmers to write code that is flexible and reusable. In Go 1.18, they’re introducing new support for generic code using parameterized types.

Golang generic [ T any ]

What is Generics ?

Golang Generics are a powerful programming feature which allows us to write generic code that can be used in multiple scenarios with different types of data. Which means ‘you can create functions and structures which don’t have to be specific for one particular type, but instead work with any type’.

This is a powerful feature that can make code more flexible, easier to write, and easier to read.

With the introduction of generics, however, it is now possible to write a single function that can work with any type. For example, you could write a function that compare two data of any type of element, not just integers.

Without Generics

Without generics, Go programmers are compelled to employ a blend of interfaces and type assertions as a means of developing code that can function with various types. For example

// multiplyInt - multiple 2 integer values
func multiplyInt(x, y int) int {
return x *y
}

// multiplyFloat32 - multiple 2 float32 values
func multiplyFloat32(x, y float32) float32 {
return x *y
}

// multiplyFloat64 - multiple 2 float64 values
func multiplyFloat64(x, y float64) float64 {
return x *y
}

How Generics Work in Go

Generics in Go are implemented using a feature called type parameters. A type parameter is a placeholder for a type that is specified when the function or data structure is used. We can write generic functions and data structures that can work with any type, rather than having to write separate functions and data structures for each type they want to work with

See the simple function which can be used to multiply with int, float32, or float64 types.

func Multiply[T int | float32 | float64 ](x,y T) T{
return x * y
}

In this example, T is a type parameter that can be int,float32 or float64 type. And these types must support multiplication.

When this function is called, the type parameter T is replaced with the actual type of the arguments passed in. For example, if the function is called with two integers, T will be replaced with the int type.

Multiply(10, 20)

We can also invoke the function by specifying the type argument.

Multiply[int](10,20)
Multiply[float32](10.12,20.30)

We can refactor this code to use the Multiply interface instead of explicitly listing out the types that the generic function accepts. This will make the code more flexible and allow it to work with any numeric type that implements the Multiply interface.

We could also use comparable built- in interface

func Multiply[T comparable](x,y T) T{
return x * y
}

comparable is an interface that is implemented by all comparable types (booleans, numbers, strings, pointers, channels, arrays of comparable types, structs whose fields are all comparable types). The comparable interface may only be used as a type parameter constraint, not as the type of a variable

or by using golang.org/x/exp/constraints

import "golang.org/x/exp/constraints"


func Multiply[T constraints.Integer](x, y T) T {
return x * y
}

Package constraints defines a set of useful constraints to be used with type parameters. Such as

Signed : (any signed integer type)

Unsigned : (any unsigned integer type)

Integer : (any integer type)

Float : (any floating-point type)

Complex : (any complex numeric type)

Ordered : (any ordered type: any type that supports the operators < <= >= >)

or by declaring your own type constraint

type customTypeConstraint interface{
int | float32 | float64
}

func Multiply[T customTypeConstraint](x, y T) T {
return x * y
}

Here are some examples of generics

With Map

// defining custom data type with generics type support
type myCustomMap[K, V comparable] map[K]V

// defining a map with Key as string and value as int
dataMap := make(myCustomMap[string,int])

dataMap["one"] = 1
dataMap["two"] = 2
println(dataMap["one"])


// defining a map with Key and value as int
dataMap := make(myCustomMap[int,int])

dataMap2[0] = 100
dataMap2[1] = 200
println(dataMap2[0])

Explanation : The myCustomMap type is a generic type that represents a map with keys of type K and values of type V.This code allows you to create custom maps with different key-value types, as long as those types satisfy the comparable constraint. Using a custom map type can improve readability and type safety in your code, especially when working with complex data structures or domain-specific logic.

With Slice

func SumOfSliceElements[K int | float32](data []K) K {
var sum K
for _, val := range data {
sum += val
}
return sum
}

func main() {
intArr := []int{
1, 2, 3, 4, 5,
}

println(
"Sum of int slice",
SumOfSliceElements(intArr),
)
floatArr := []float32{
1.0, 2.2, 3.3, 4.4, 5.5,
}

println(
"Sum of floatArr slice",
SumOfSliceElements(floatArr),
)
}

Explanation : The function uses the K generic type parameter to accept either int or float32 values. The generic type parameter is constrained to only these two types using the | operator in the function signature.

Inside the function, created a sum variable of type K and iterate over each value in the input data slice. We add each value to the sum variable using the += operator, which is supported by both int and float32. Finally, we return the computed sum.

With Struct

type userInterface interface {
constraints.Ordered | []byte
}

type User[T userInterface] struct {
ID int
Name string
Data T
}

func main() {
u := User[int]{
ID: 1,
Name: "name",
Data: 113,
}
fmt.Println(u)

// creating another object with Data as string
u1 := User[string]{
ID: 1,
Name: "name",
Data: "sss",
}
fmt.Println(u1)

}

Explanation : The User struct represents a user with an ID, name, and additional data of any type that conforms to the userInterface interface. The userInterface interface is a combination of two interfaces. This means that any value of type T that is used as the Data field in the User struct must implement both constraints.Ordered and []byte interfaces.

With Function

func SumOfSliceElements[K int | float32](data []K) K {
var sum K
for _, val := range data {
sum += val
}
return sum
}

SumOfSliceElements, which takes a slice of elements of a generic type parameter K. The generic type parameter is constrained to be either int or float32.

By constraining the generic type parameter to numeric types, the function can ensure that the + operator is available for addition. This allows the function to work with slices of integers or floating-point numbers.

With Methods

// Define a generic type using a type parameter
type Product[T any] struct {
SerialNumber T
}

// SetSerialNumber
func (t *Product[T]) SetSerialNumber(number T) {
t.SerialNumber = number
}

// GetSerialNumber
func (t *Product[T]) GetSerialNumber() T {
return t.SerialNumber
}

func main() {
// Create an instance of Product with an serialnumber as int32
product1 := Product[int]{}

product1.SetSerialNumber(130458445)
serialNumber1 := product1.GetSerialNumber()

fmt.Println("Serial : ", serialNumber1)

// Create an instance of Product with an serialnumber as int32
product2 := Product[string]{}

product2.SetSerialNumber("#130458445")
serialNumber2 := product2.GetSerialNumber()

fmt.Println("Serial : ", serialNumber2)
}

Explanation: Product[T any] is a generic struct that represents a product with a serial number of type T. It can be used to represent any type of product, where the serial number can be any valid Go data type.

When Not to use generics

As a general rule, if you observe a lot of repeated code blocks, consider replacing them with generic code. If the code you’re building can be limited to only a few kinds, interfaces could be a better choice.

Conclusion

In conclusion, the recent addition of generics in the release of Go, version 1.18, is a significant step toward improving developer efficiency and minimizing chances of inconsistencies while programing. With support for generic programming functionality, programmers can now avoid writing specific codes for different types but instead manipulate them as general entities increasing program flexibility and re-usability. Concisely written and error-free code is now within reach for Go programmers with the inclusion of generics, leading to more dependable software tools developed at breakneck speeds.

I hope this article helps. Thank you so much for reading. :)

Feel free to share your thoughts or experiences.

Read More

--

--

PRATHEESH PC

Hey there! I'm a software developer and I'm absolutely in love with building things with technology.