Getting Started with Go Generics: A Quick Overview
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.
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 typeK
and values of typeV.
This code allows you to create custom maps with different key-value types, as long as those types satisfy thecomparable
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 eitherint
orfloat32
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 typeK
and iterate over each value in the inputdata
slice. We add each value to thesum
variable using the+=
operator, which is supported by bothint
andfloat32
. Finally, we return the computedsum
.
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 theuserInterface
interface. TheuserInterface
interface is a combination of two interfaces. This means that any value of typeT
that is used as theData
field in theUser
struct must implement bothconstraints.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 typeT
. 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