Generics in Go

This blog explores Go's exciting feature - generics. It covers their implementation with practical examples like Stack and Map. The post highlights benefits, constraints, and potential challenges, ultimately celebrating generics' power and flexibility in Go.

GraphQL has a role beyond API Query Language- being the backbone of application Integration
background Coditation

Generics in Go

Today, we embark on an exploration of the exciting Go's arsenal - generics. In the version 1.18 release, Go introduces generics, bringing flexibility and power to the language. While not groundbreaking in the programming world, generics' inclusion in Go has sparked curiosity and excitement. In this blog post, we demystify Go's generics and unveil how they revolutionize our code. Get ready to elevate your programming skills with reusable, concise, and efficient Go code. Discover Go's take on generics, designed with simplicity and readability in mind. We'll dive into key concepts, syntax examples, and the benefits they bring.

So before proceeding to know how we can implement generics in Go, let’s discuss an overview of generics, and going ahead, we will discuss generics in Go itself.

So, what exactly are generics? 

Generics are a programming feature that allows developers to write reusable code that can work with various types of data without having to rewrite the code for each type. You can use generics to define a class, interface, or method that can operate on objects of different types, which are specified as type parameters when the code is compiled. This results in improved type safety, performance, and code readability.
A generic class like "List<T>" for example, can be used to represent a list of any type of data, such as integers, strings, or custom objects. You specify the type of data that a "List<T>" should hold when you instantiate it, such as "List<Integer>" or "List<String>". This makes the code more readable because it is clear what type of data is being manipulated, and it reduces the likelihood of type errors occurring at runtime. In simply words provide any type of data compiler will find its type and will do the operation accordingly
Generics allow you to write code that can work with multiple data types rather than just one. This enables you to write more reusable, cleaner, and efficient code, as well as catch errors at compile time rather than runtime. You can ensure that the correct data is being passed and avoid type mismatches by specifying the type of data a specific piece of code is designed to work with. Generics also improve code readability, making it easier for other developers to understand what is intended. Generics are a powerful tool for improving code quality and making it more flexible and maintainable.
So, where question comes how can we utilise this generics in Golang, Let’s do this with so real world examples.
We’ll go around the major data types in Go and how we can implement them with help of generics
Let’s take a very simple example of a slice here. 
PrintSlice is a generic function that prints the value provided in slice. type of slice provided, so in this way we can provide different types of data types to function in Go.
Example : https://go.dev/play/p/RwFOll1vEHd


package main

import "fmt"

// Stack is a generic stack implementation
type Stack[T any] []T

// Push adds an element to the stack
func (s *Stack[T]) Push(elem T) {
	*s = append(*s, elem)
}

// Pop removes and returns the top element from the stack
func (s *Stack[T]) Pop() T {
	if s.IsEmpty() {
		panic("Stack is empty")
	}

	index := len(*s) - 1
	elem := (*s)[index]
	*s = (*s)[:index]

	return elem
}

// IsEmpty checks if the stack is empty
func (s *Stack[T]) IsEmpty() bool {
	return len(*s) == 0
}

func main() {
	// Creating an integer stack
	intStack := new(Stack[int])
intStack.Push(10)
	intStack.Push(20)
	intStack.Push(30)

	for !intStack.IsEmpty() {
		fmt.Println(intStack.Pop())
	}

	// Creating a string stack
	strStack := new(Stack[string])

	strStack.Push("Hello")
	strStack.Push("World")

	for !strStack.IsEmpty() {
		fmt.Println(strStack.Pop())
	}
}

In this example, we define a generic stack data structure using the power of generics in Go. The Stack type is defined as a slice of a generic type T. We then provide methods such as Push, Pop, and IsEmpty to manipulate the stack. We demonstrate the usage of the generic stack by creating both an integer stack and a string stack. We push some elements onto each stack and then pop and print the elements until the stack becomes empty. This example showcases how generics allow us to create reusable and type-safe data structures and algorithms.
So another majorly used data type in Go is map, with generics map is powered with lot of usages, let’s see an working example of maps with generics.


// V - V is type that function accepted, In this cases its array of T i.e type
// any - any represent `any` value i.e interface incase of Go
// comparable -  is the constraint for types that support equality operators == and !=. 
func MapKeys[K comparable, V any](m map[K]V) []K {
	r := make([]K, 0, len(m))
	for k := range m {
		r = append(r, k)
	}
	return r
}

In the above code, it will return the keys provided in the map.
struct is another data type that we can use


// Generic struct with two generic types
type GenericStruct[K, V any] struct {
	Data1 []K
	Data2 []V
}

func main() {
	modelInt := GenericStruct[int, string]{Data1: []int{1, 2}, Data2: []string{"Hi", "Go"}}
	fmt.Println(modelInt)
}

Also we can use predefined types in constraints, we can have set of data types and aggregate them with help of interface and use.


type Number interface {
	int | int8 | int16 | int32 | int64 | float32 | float64
}

func Equal[T Number](a, b T) bool {
	if a == b {
		return true
	}
	return false
}

Operator allows for type unions. We can put the similar types which supports the common operations in this case we can use equal,min,max etc. with numbers.

Common Challenges/Limitations and Pitfalls/Risks When Using Generics with Go

    1. Limited type constraints: Generics in Go have constraints that are less comprehensive than in other languages, lacking the ability to specify constraints based on interfaces.

    2. Compatibility and package ecosystem: The introduction of generics in Go can disrupt compatibility with existing code and packages reliant on reflection or dynamic type checks, requiring updates to ensure compatibility.

    3. Increased compile times: Utilizing generics in Go leads to longer compilation times as the compiler needs to comprehend and generate specialized code for each generic type.

    4. Complexity and cognitive load: Introducing generics adds complexity to the codebase, posing challenges for developers, especially those unfamiliar with generics.

    5. Debugging and error messages: Troubleshooting and identifying errors in generic code is more difficult due to increased complexity, resulting in less clear or intuitive error messages.

Conclusion

In summary, Go's introduction of generics has added significant power and flexibility to the language. It enables developers to write reusable code that can handle multiple data types, resulting in improved code readability and efficiency. I recently utilized generics in Go to implement a Priority Queue data structure. By leveraging generics, I created a PriorityQueue type that could handle elements of any comparable type. This eliminated the need for duplicating code and allowed me to create priority queues for different types without sacrificing type safety. The use of generics made my code more concise and maintainable by promoting code reuse and catching errors during compilation. It also improved performance as the compiler generated specialized code for each specific type, resulting in efficient execution. Overall, generics in Go have greatly enhanced my programming experience by enabling me to write cleaner, more reusable, and type-safe code. It has proven to be a valuable tool for building robust and efficient software solutions in Go.

Want to receive update about our upcoming podcast?

Thanks for joining our newsletter.
Oops! Something went wrong.

Latest Articles

Implementing Custom Instrumentation for Application Performance Monitoring (APM) Using OpenTelemetry

Application Performance Monitoring (APM) has become crucial for businesses to ensure optimal software performance and user experience. As applications grow more complex and distributed, the need for comprehensive monitoring solutions has never been greater. OpenTelemetry has emerged as a powerful, vendor-neutral framework for instrumenting, generating, collecting, and exporting telemetry data. This article explores how to implement custom instrumentation using OpenTelemetry for effective APM.

Mobile Engineering
time
5
 min read

Implementing Custom Evaluation Metrics in LangChain for Measuring AI Agent Performance

As AI and language models continue to advance at breakneck speed, the need to accurately gauge AI agent performance has never been more critical. LangChain, a go-to framework for building language model applications, comes equipped with its own set of evaluation tools. However, these off-the-shelf solutions often fall short when dealing with the intricacies of specialized AI applications. This article dives into the world of custom evaluation metrics in LangChain, showing you how to craft bespoke measures that truly capture the essence of your AI agent's performance.

AI/ML
time
5
 min read

Enhancing Quality Control with AI: Smarter Defect Detection in Manufacturing

In today's competitive manufacturing landscape, quality control is paramount. Traditional methods often struggle to maintain optimal standards. However, the integration of Artificial Intelligence (AI) is revolutionizing this domain. This article delves into the transformative impact of AI on quality control in manufacturing, highlighting specific use cases and their underlying architectures.

AI/ML
time
5
 min read