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 feature flags for controlled rollouts and experimentation in production

Discover how feature flags can revolutionize your software deployment strategy in this comprehensive guide. Learn to implement everything from basic toggles to sophisticated experimentation platforms with practical code examples in Java, JavaScript, and Node.js. The post covers essential implementation patterns, best practices for flag management, and real-world architectures that have helped companies like Spotify reduce deployment risks by 80%. Whether you're looking to enable controlled rollouts, A/B testing, or zero-downtime migrations, this guide provides the technical foundation you need to build robust feature flagging systems.

time
12
 min read

Implementing incremental data processing using Databricks Delta Lake's change data feed

Discover how to implement efficient incremental data processing with Databricks Delta Lake's Change Data Feed. This comprehensive guide walks through enabling CDF, reading change data, and building robust processing pipelines that only handle modified data. Learn advanced patterns for schema evolution, large data volumes, and exactly-once processing, plus real-world applications including real-time analytics dashboards and data quality monitoring. Perfect for data engineers looking to optimize resource usage and processing time.

time
12
 min read

Implementing custom embeddings in LlamaIndex for domain-specific information retrieval

Discover how to dramatically improve search relevance in specialized domains by implementing custom embeddings in LlamaIndex. This comprehensive guide walks through four practical approaches—from fine-tuning existing models to creating knowledge-enhanced embeddings—with real-world code examples. Learn how domain-specific embeddings can boost precision by 30-45% compared to general-purpose models, as demonstrated in a legal tech case study where search precision jumped from 67% to 89%.

time
15
 min read