DEV Community

Rahul sawra
Rahul sawra

Posted on

Harnessing the Power of Generics in Go: A Comprehensive Guide - Part I

Introduction

Go,has gained significant popularity for its simplicity, efficiency, and concurrency features. However, one critical feature that has been missing for a long time was generics. Generic programming enables developers to write code that can work with different data types, making code more flexible, reusable, and less error-prone. With the introduction of generics in Go 1.18, the language has taken a giant leap forward, empowering developers to tackle complex problems with greater elegance and efficiency. In this blog post, we'll dive into the world of generics in Go and explore how this new feature revolutionizes the way we write code.

The Need for Generics

Before the introduction of generics in Go, developers had to rely on interfaces or write type-specific code to handle various data types. While interfaces allowed some level of abstraction, they often introduced performance overhead and forced developers to compromise on type safety. Additionally, code duplication was common when dealing with multiple data types, leading to maintainability challenges.

Generics address these limitations by enabling the creation of functions, data structures, and algorithms that work with any type while maintaining strict type safety. This means developers can write code once and apply it to multiple data types, resulting in cleaner, more maintainable, and efficient programs.

Type Parameters and Type Constraints

At the core of generics are type parameters, which are placeholders for specific types used within a generic function or data structure.
Functions and types are now permitted to have type parameters. A type parameter list looks like an ordinary parameter list, except that it uses square brackets instead of parentheses.

To show how this works, let’s start with the basic non-generic Min function for floating point values:

func Min(x, y float64) float64 {
    if x < y {
        return x
    }
    return y
}
Enter fullscreen mode Exit fullscreen mode

The above function Min() only works for floating type values. In order to calculate Min() of integers you need to write a seperate function (or use interfaces).

We can make this function generic–make it work for different types–by adding a type parameter list. In this example we add a type parameter list with a single type parameter T, and replace the uses of float64 with T.

import "golang.org/x/exp/constraints"

func GMin[T constraints.Ordered](x, y T) T {
    if x < y {
        return x
    }
    return y
}
Enter fullscreen mode Exit fullscreen mode

It is now possible to call this function with a type argument by writing a call like

x := GMin[int](2, 3)
y := GMin[float64](2.1, 3.1)

Type constraints play a crucial role in ensuring type safety while using generics. By specifying constraints, developers limit the types that can be used with a generic function, preventing misuse and providing better error messages during compilation.

In the generic GMin implementation, the type constraint is obtained from the constraints package.
constraints.Ordered is a type constraint.

Specifically, the Ordered constraint is responsible for defining the set of types whose values can be ordered, i.e., compared using operators such as <, <=, >, etc. This constraint guarantees that only types with comparable values can be used in GMin.

It's important to note that in Go, type constraints are required to be implemented as interfaces.

We look at the interfaces in a new way now.
Until recently, the Go spec said that an interface defines a method set, which is roughly the set of methods enumerated in the interface. Any type that implements all those methods implements that interface.

Go Interfaces

You can now define type set in a interface. For instance, interface{ int|string|bool } defines the type set containing the types int, string, and bool.

type-sets

Another way of saying this is that this interface is satisfied by only int, string, or bool.
Now let’s look at the actual definition of constraints.Ordered:

type Ordered interface {
    Integer|Float|~string
}
Enter fullscreen mode Exit fullscreen mode

This declaration says that the Ordered interface is the set of all integer, floating-point, and string types. The vertical bar expresses a union of types. Integer and Float are interface types that are similarly defined in the constraints package. Note that there are no methods defined by the Ordered interface.

Now let's look at a simple example where a func SumIntsOrFloats is a generic function that sums the values of map m. It supports value of both types int64 and float64.

package main

import (
    "fmt" 
)

// SumIntsOrFloats sums the values of map m. It supports both int64 and float64
// as types for map values.
// K avd V are type params
// comparable = type constraint (built in)
// Number = type constraint
type Number interface {
    int64 | float64
}

func SumIntsOrFloats[K comparable, V Number](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}

func main() {
    // Initialize a map for the integer values
    ints := map[string]int64{
        "first":  34,
        "second": 12,
    }

    // Initialize a map for the float values
    floats := map[string]float64{
        "first":  35.98,
        "second": 26.99,
    }
    fmt.Printf("Generic Sums: %v and %v\n",
    SumIntsOrFloats(ints),
    SumIntsOrFloats(floats))
}
Enter fullscreen mode Exit fullscreen mode
go run main.go 
Generic Sums: 46 and 62.97
Enter fullscreen mode Exit fullscreen mode

Well, that's all about generics in this blog. We will dive deep into generics in coming blogs and look at more use cases.

Reach out to me, if you want to discuss anything about GO! :D
Twitter - https://twitter.com/im_rsawra
LinkedIn - https://www.linkedin.com/in/rahul-sawra/

Ref:

  1. https://go.dev/blog/intro-generics
  2. https://youtu.be/Pa_e9EeCdy8

Top comments (0)