Go-lang is a statically-typed language, which means that all variables and function parameters must be pre-defined else it will raise a compilation error.
The standard approach of developing reusable code that can accept different types would be to duplicate the same code but with different input types e.g. a function that performs division with 2 integers would need to be re-written to accept 2 float64 parameters. In fact, this is how some of the built-in library functions such as map
, reduce
and filter
were initially developed by duplication to support different types of slices. Another approach could be to use reflection
but it would incur additional computational costs if type checking has to be invoked for every function call for every type.
Generics were developed to solve this issue. Generics allows for code reuse and provides mechanism to implement data structures which could be used for multiple types. In addition, using Generics also allows you to detect incompatible types at runtime.
A good example of a data structure would be a Stack. A stack has a LIFO ( Last-In First-Out ) order where data inserted last is removed first. Assuming we have an initial stack type that accepts integers:
The code above can be found at this go playground
If the stack were to provide support for float types, we would need to duplicate and implement the stack to hold float types. This is error-prone and not DRY.
Generics can be used in the following contexts in go:
- custom types and data structures
- interfaces
- functions
To create a generic Stack:
The code above can be found at this go playgound
There are 2 built-in generic types we can use: any
or comparable
. any
refers to any unspecified type supported by the language. comparable
refers to any type that implements a comparison operator such as the equality operator. To use the stack to store integers, we define a stack via Stack[int]
and a stack of floats can be defined as Stack[float64]
without having to duplicate the stack code.
In addition, we get validation for free. Given the above example, if we accidentally tried to add a float into the first stack, we will see an error:
Generics can also be applied to functions. For example, suppose there is a requirement to define a generic function that accepts an int or float parameter only and doubles its value.
We define a custom interface that accepts only numeric types. The generic function is defined to only accept the custom interface as the generic type:
The code above can be found at this go playground
Another example is to apply generics to interfaces. For example, we can create a generic interface called Printable that accepts a type which implements fmt.Stringer and can only be an underlying type of int, float64, or Person:
In the example above, we define a Printable interface which only accepts int, float64 and a custom Person struct. We also stipulated that the types must implement the fmt.Stringer interface, which means the types must implement the String() function that returns a string. We define custom integer and float64 types that implement the String() function. The code can be accessed via this go playground
In this short post, I aim to introduce the use of generics in go-lang to create reusable data structures and functions. Generics is an advanced subject and readers are encouraged to explore more by reading the go lang blog to find out more.