Home >Backend Development >Golang >Generics in Go: Transforming Code Reusability
Generics, introduced in Go 1.18, have revolutionised the way of writing reusable and type-safe code. Generics bring flexibility and power while maintaining Go’s philosophy of simplicity. However, understanding nuances, benefits, and how generics compare to traditional approaches (like interface{} ) requires a closer look.
Let’s explore the intricacies of generics, delve into constraints, compare generics to interface{}, and demonstrate their practical applications. We’ll also touch upon performance considerations and binary size implications. Let’s dive in!
Generics allow developers to write functions and data structures that can operate on any type while maintaining type safety. Instead of relying on interface{}, which involves type assertions in runtime, generics let you specify a set of constraints that dictate the permissible operations on the types.
Syntax
func FunctionName[T TypeConstraint](parameterName T) ReturnType { // Function body using T }
T: A type parameter, representing a placeholder for the type.
TypeConstraint: Restricts the type of T to a specific type or a set of types.
parameterName T: The parameter uses the generic type T.
ReturnType: The function can also return a value of type T.
Example
func Sum[T int | float64](a, b T) T { return a + b }
func Sum: Declares the name of the function, Sum
[T int | float64]: Specifies a type parameter list that introduces T as a type parameter, constrained to specific types (int or float64). Sum function can take only parameters either int or float64, not in combination, both have to either int or float64. We will explore this further in below sections.
(a, b T): Declares two parameters, a and b, both of type T (the generic type).
T: Specifies the return type of the function, which matches the type parameter T.
Constraints define what operations are valid for a generic type. Go provides powerful tools for constraints, including the experimental constraints package(golang.org/x/exp/constraints).
Go introduced built-in constraints with generics to provide type safety while allowing flexibility in defining reusable and generic code. These constraints enable developers to enforce rules on the types used in generic functions or types.
func FunctionName[T TypeConstraint](parameterName T) ReturnType { // Function body using T }
func Sum[T int | float64](a, b T) T { return a + b }
Experimental constraints
func PrintValues[T any](values []T) { for _, v := range values { fmt.Println(v) } }
Custom constraints are interfaces that define a set of types or type behaviours that a generic type parameter must satisfy. By creating your own constraints, we can;
Restrict types to a specific subset, such as numeric types.
Require types to implement specific methods or behaviors.
Add more control and specificity to your generic functions and types.
Syntax
func CheckDuplicates[T comparable](items []T) []T { seen := make(map[T]bool) duplicates := []T{} for _, item := range items { if seen[item] { duplicates = append(duplicates, item) } else { seen[item] = true } } return duplicates }
Example
import ( "golang.org/x/exp/constraints" "fmt" ) func SortSlice[T constraints.Ordered](items []T) []T { sorted := append([]T{}, items...) // Copy slice sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] }) return sorted } func main() { nums := []int{5, 2, 9, 1} fmt.Println(SortSlice(nums)) // Output: [1 2 5 9] words := []string{"banana", "apple", "cherry"} fmt.Println(SortSlice(words)) // Output: [apple banana cherry] }
Sum function can be called using only int, int64 and float64 parameters.
If you want to enforce a type must implement certain methods, you can define it using those methods.
type Numeric interface { int | float64 | uint }
The Formatter constraint requires that any type used as T must have a Format method that returns a string.
Custom constraints can combine type sets and method requirements
type Number interface { int | int64 | float64 } func Sum[T Number](a, b T) T { return a + b }
This constraint includes both specific types (int, float54) and requires the presence of an abs method.
Before introduction of generics, interface{} was used to achieve flexibility. However, this approach has limitations.
interface{}: Relies on runtime type assertions, increasing the chance of errors at runtime.
Generics: Offers compile-time type safety, catching errors early during development.
interface{}: Slower due to additional runtime type checks.
Generics: Faster, as the compiler generates optimised code paths specific to types.
interface{}: Often verbose and less intuitive, making the code harder to maintain.
Generics: Cleaner syntax leads to more intuitive and maintainable code.
interface{}: Results in smaller binaries as it doesn’t duplicate code for different types.
Generics: Slightly increases binary size due to type specialisation for better performance.
Example
func FunctionName[T TypeConstraint](parameterName T) ReturnType { // Function body using T }
Code works well, type assertion is overhead. Add function can called with any argument, both a and b parameters can be of different types, however code will crash in the runtime.
func Sum[T int | float64](a, b T) T { return a + b }
Generics eliminate the risk of runtime panics caused by incorrect type assertions and improve clarity.
Generics produce specialised code for each type, leading to better runtime performance compared to interface{}.
A trade-off exists: generics increase binary size due to code duplication for each type, but this is often negligible compared to the benefits.
Complexity in Constraints: While constraints like constraints.Ordered simplify common use cases, defining highly customized constraints can become verbose.
No Type Inference in Structs: Unlike functions, you must specify the type parameter explicitly for structs.
func PrintValues[T any](values []T) { for _, v := range values { fmt.Println(v) } }
Limited to Compile-Time Constraints: Go generics focus on compile-time safety, whereas languages like Rust offer more powerful constraints using lifetimes and traits.
We will implement a simple Queue with both interface{} and generic and benchmark the results.
func CheckDuplicates[T comparable](items []T) []T { seen := make(map[T]bool) duplicates := []T{} for _, item := range items { if seen[item] { duplicates = append(duplicates, item) } else { seen[item] = true } } return duplicates }
import ( "golang.org/x/exp/constraints" "fmt" ) func SortSlice[T constraints.Ordered](items []T) []T { sorted := append([]T{}, items...) // Copy slice sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] }) return sorted } func main() { nums := []int{5, 2, 9, 1} fmt.Println(SortSlice(nums)) // Output: [1 2 5 9] words := []string{"banana", "apple", "cherry"} fmt.Println(SortSlice(words)) // Output: [apple banana cherry] }
type Numeric interface { int | float64 | uint }
Execution Time:
The generic implementation is approximately 63.64% faster than the interface{} version because it avoids runtime type assertions and operates directly on the given type.
Allocations:
The interface{} version makes 3x more allocations, primarily due to boxing/unboxing when inserting and retrieving values. This adds overhead to garbage collection.
For larger workloads, such as 1 million enqueue/dequeue operations, the performance gap widens. Real-world applications with high-throughput requirements (e.g., message queues, job schedulers) benefit significantly from generics.
Generics in Go strike a balance between power and simplicity, offers a practical solution for writing reusable and type-safe code. While not as feature-rich as Rust or C , align perfectly with Go’s minimalist philosophy. Understanding constraints like constraints.Ordered and leveraging generics effectively can greatly enhance code quality and maintainability.
As generics continue to evolve, they are destined to play a central role in Go’s ecosystem. So, dive in, experiment, and embrace the new era of type safety and flexibility in Go programming!
Checkout github repository for some samples on generics.
Welcome to the Go Generics Repository! This repository is a one-stop resource for understanding, learning, and mastering generics in Go, introduced in version 1.18. Generics bring the power of type parameters to Go, enabling developers to write reusable and type-safe code without compromising on performance or readability.
This repository contains carefully curated examples that cover a wide range of topics, from basic syntax to advanced patterns and practical use cases. Whether you're a beginner or an experienced Go developer, this collection will help you leverage generics effectively in your projects.
These examples introduce the foundational concepts of generics, helping you grasp the syntax and core features:
The above is the detailed content of Generics in Go: Transforming Code Reusability. For more information, please follow other related articles on the PHP Chinese website!