Beginner's Guide to Generics in Golang

Generics in Go refers to a language feature that allows creating functions, data structures, and interfaces that can work with different types. In other words, generics enable the creation of code that is not limited to a specific type or data structure.

Before the introduction of generics in Go, developers had to write multiple functions to handle different types of data. This approach was often cumbersome and led to code duplication. With generics, developers can write more concise and reusable code that can handle different types of data.

Generics in Go were introduced in version 1.18, which was released in February 2021. The implementation of generics in Go is based on the concept of type parameters. Type parameters are placeholders for types that are passed as arguments to a function or a data structure, allowing them to work with different types of data.

Table Of Contents

  1. What are Generic types in Go?
  2. How Generics works in Go?
  3. What are Type Parameters?
  4. Using Type Parameters in Generics
  5. Type Constraints
  6. Examples of Using Generics in Golang
  7. Limitations of Generics

What are Generic types in Go?

Generic types (simply called Generics) are codes that allow us to use them for various functions by just altering the function types. Generics were created to make code independent of types and functions.

The main aim of generics is to achieve greater flexibility in terms of writing code with the addition of fewer lines.

To understand better, look at this example below. We make a function for printing any type parameter like this:

func Print(s[] string) {
    for _, v: = range s {
        fmt.Print(v)
    }
}

Now, instead of printing a string, we suddenly wish to print an integer, so we change the code accordingly.

func Print(s[] int) {
    for _, v: = range s {
        fmt.Print(v)
    }
}

But changing the code every time like this may seem daunting and this is where Generics come into play. By assigning any type in its generic form, we can make use of the same code for different functions. Take a look at this:

func Print[T any](s[] T) {
    for _, v: = range s {
        fmt.Print(v)
    }
}

Here we have assigned "T" as an any keyword. This any type allows us to parse different types of variables within the same function. S is the corresponding variable, which is a slice of T. Now, calling the method we can print a string and an integer together in the same function.

func main() {
    str := []string{"Hello", "Again Hello"]
    intArray := []int {1, 2, 3}
    Print(str)
    Print(intArray)
}

How Generics works in Go?

Generics in Go are implemented using type parameters, which allow for the creation of generic functions and data structures that can operate on different types without the need for explicit type conversions.

Consider this example where the type parameter "T" is defined using the "any" keyword, which specifies that any type can be used with this function.

func Swap[T any](a, b * T) { * a, * b = * b, * a
}

The function body then performs a simple swap of the values pointed to by the two pointers passed in as arguments.

When the function is called, the compiler generates a specific version of the function for the type that is used with the function. For example, if the function is called with two pointers to integers, the compiler generates a version of the function that operates on integers.

Generics in Go also support type constraints, which allow for more specific types to be used with generic functions and data structures. Type constraints are specified using the "interface" keyword, followed by the name of the interface and the methods that the type must implement.

For example, the following generic function defines a type constraint that requires the type parameter to be a slice of integers:

func Sum[T Slice[Int]](slice T) int {
    sum: = 0
    for _,
    v: = range slice {
        sum += v
    }
    return sum
}

Here, the Slice[Int] constraint ensures that only slices of integers can be used with the "Sum" function.

We have seen how important Type Parameters and Type Constraints are in Generics. In the following sections, we will take a deeper look into their definition and working.

What are Type Parameters?

In Go, type parameters are specified using a type parameter list enclosed in square brackets immediately following the name of the function, data structure, or interface. The type parameters are represented by a single uppercase letter or a sequence of uppercase letters, enclosed in angle brackets.

Type parameters are used to create generic functions, data structures, and interfaces in Go. Type parameters are placeholders for types that are determined at compile-time.

For example, consider the above example that shows a function declaration that uses a type parameter. In this function, the type parameter is represented by the uppercase letter "T". The "any" keyword indicates that the function can work with any type. When this function is called, the type parameter is replaced with the actual type of the argument passed to the function.

Type parameters enable the creation of more generic and reusable code in Go by allowing functions and data structures to work with different types of data.

Using Type Parameters in Generics

In the example that we saw above, we saw how to incorporate more than one type of variable under the same function.

In the example, the function is declared with a type parameter "T" using the "any" keyword. The "any" keyword indicates that the function can work with any type. The function takes a slice of type "T" as an argument and prints its contents.

To use this function, you can call it with a slice of any type like what’s given below:

intSlice: = [] int {
    1, 2, 3, 4, 5
}
stringSlice: = [] string {
    "apple", "banana", "cherry"
}
PrintSlice(intSlice) // prints 1 2 3 4 5
PrintSlice(stringSlice) // prints apple banana cherry

In this example, the PrintSlice function is called with a slice of integers and a slice of strings. The type parameter "T" is replaced with the actual types of the arguments passed to the function.

You can also create generic data structures and interfaces in Go using type parameters. Here's an example of a generic data structure, a stack, that uses a type parameter:

type Stack[T any] struct {
    items[] T
}
func(s * Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}
func(s * Stack[T]) Pop() T {
    if len(s.items) == 0 {
        panic("stack is empty")
    }
    item: = s.items[len(s.items) - 1]
    s.items = s.items[: len(s.items) - 1]
    return item
}
  • Here, the Stack data structure is declared with a type parameter "T" using the "any" keyword.
  • The Push method takes an item of type "T" as an argument and adds it to the stack.
  • The Pop method returns an item of type "T" from the top of the stack.

To use this data structure, you can create a stack of any type:

intStack: = & Stack[int] {}
stringStack: = & Stack[string] {}
intStack.Push(1)
intStack.Push(2)
intStack.Push(3)
stringStack.Push("apple")
stringStack.Push("banana")
stringStack.Push("cherry")
fmt.Println(intStack.Pop()) // prints 3
fmt.Println(stringStack.Pop()) // prints cherry

In this example, two stacks are created, one of type int and one of type string. The type parameter "T" is replaced with the actual types of the stacks create

Type Constraints

Type constraints in generics define the set of types that can be used with a generic function or data structure. Type constraints allow the compiler to enforce type safety and ensure that only compatible types are used with a generic construct.

Type constraints are specified using the "interface" keyword, followed by the name of the interface and the methods that the type must implement. For example, consider the following generic function that uses a type constraint:

func Max[T comparable](a, b T) T {
    if a > b {
        return a
    }
    return b
}

In this example, the type parameter "T" is constrained by the "comparable" interface, which requires that the type implements the comparison operators (>, <, >=, <=). This ensures that the function can only be called with types that support comparison.

comparable is a built-in interface that is used to constrain generic type parameters to only those types that support comparison operators (<, <=, >, >=, and ==).

The comparable interface is implicitly defined by the Go language specification and is not required to be explicitly defined in code. This means that any type that supports comparison operators can be used as a type parameter for the Max function without any additional declaration of the comparable interface.

Type constraints can also be user-defined interfaces, which allow for more specific constraints on the types that can be used with a generic function or data structure. For example, consider the following user-defined interface:

type Number interface {
    Add(other Number) Number
    Sub(other Number) Number
    Mul(other Number) Number
    Div(other Number) Number
}

This interface defines a set of methods that a type must implement in order to be considered a "Number". A generic function or data structure that uses this interface as a type constraint can only be used with types that implement these methods, ensuring type safety and compatibility.

Type constraints in generics in Go provide a way to ensure type safety and limit the set of types that can be used with a generic construct, while still allowing for the flexibility and reusability that generics provide.

Examples of Using Generics in Golang

Here are some examples of using generics in Go:

1. Generic functions:

This function takes a slice of any type T and a value of type T and returns the index of the value in the slice. The any keyword in the type parameter specifies that any type can be used.

func findIndex[T any](slice []T, value T) int {
    for i, v := range slice {
        if reflect.DeepEqual(v, value) {
            return i
        }
    }
    return -1
}
Generic Function in Golang
Generic Function in Golang

2. Generic types:

This defines a generic stack type that can hold elements of any type T. The any keyword specifies that any type can be used as the element type.

type Stack[T any] []T

func (s *Stack[T]) Push(value T) {
    *s = append(*s, value)
}

func (s *Stack[T]) Pop() T {
    if len(*s) == 0 {
        panic("Stack is empty")
    }
    value := (*s)[len(*s)-1]
    *s = (*s)[:len(*s)-1]
    return value
}
Generic Types in Golang
Generic Types in Golang

3. Constraints on type parameters:

This defines a type constraint on the type parameter T that requires it to implement the Equatable interface. This allows the findIndex function to use the Equals method to compare values of type T.

type Equatable interface {
    Equals(other interface{}) bool
}

func findIndex[T Equatable](slice []T, value T) int {
    for i, v := range slice {
        if v.Equals(value) {
            return i
        }
    }
    return -1
}
Constraints on Type Parameter
Constraints on Type Parameter

These are just a few examples of how generics can be used in Go to write more flexible and reusable code.

Limitations of Generics

Although generics in Go have brought many benefits and new possibilities to the language, there are still some limitations and challenges that come with their implementation. Here are some of the main limitations of generics in Go:

  1. Performance: One of the main concerns with generics in Go is the potential impact on performance. With the introduction of generics, the Go compiler needs to generate code for different types at compile time, which can lead to larger binaries and slower compilation times.
  2. Type constraints: Go's implementation of generics relies on type constraints to ensure type safety. However, these constraints can be restrictive and limit the types that can be used with generic functions and data structures.
  3. Syntax complexity: The syntax for declaring and using generic functions and data structures can be complex and difficult to understand, especially for beginners.
  4. Error messages: The error messages generated by the Go compiler for issues related to generics can be difficult to understand, making debugging and troubleshooting more challenging.
  5. Code readability: Generics in Go can sometimes make code less readable and harder to understand, especially if type constraints and type parameters are used extensively.
  6. No switching possible: When you want to switch from one underlying generic type to another, it is not possible using generics. The only way to go about this is to use an interface and run the type switch function at runtime.
func is64Bit[T Float](v T) T {
    switch (interface {})(v).(type) {
        case float32:
            return false
        case float64:
            return true
    }
}

Final Thoughts

Generics provide a powerful yet simple method for creating generic interfaces, methods for structs, and functions.

They enable a reduction in redundant information and, at least in some instances, provide a superior alternative to reflection. Of course, the primary reason generics were vehemently opposed for a considerable amount of time was that they could make code more difficult to read and parse, which would appear to go against the simplicity of Go.

Generics, on the other hand, are an excellent and necessary addition to the language if used wisely and where it makes sense.


Monitor Your Go Applications with Atatus

Atatus provides developers with insights into the performance of their Golang applications. By tracking requests and backend performance, Atatus helps identify bottlenecks in the application and enables developers to diagnose and fix errors more efficiently.

Golang monitoring
Golang monitoring

Golang monitoring captures errors and exceptions that occur in Golang applications and provides a detailed stack trace, enabling developers to quickly pinpoint the exact location of the error and address it.

We provide flexible alerting options, such as email, Slack, PagerDuty, or webhooks, to notify developers of Golang errors and exceptions in real-time. This enables developers to address issues promptly and minimize any negative impact on the end-user experience.

Try Atatus’s entire features free for 14 days.

Atatus

#1 Solution for Logs, Traces & Metrics

tick-logo APM

tick-logo Kubernetes

tick-logo Logs

tick-logo Synthetics

tick-logo RUM

tick-logo Serverless

tick-logo Security

tick-logo More

Aiswarya S

Aiswarya S

Writes on SaaS products, the newest observability tools in the market, user guides and more.