Engineering Blog

                            

Being confused about when to use generics

Introduction

The Go 1.18 release introduced a new feature called generic types (commonly known by the shorter term, generics). This allows writing code with types that can be specified later and instantiated when needed. However, it can be confusing about when to use generics and when not to. In this blog, I will try to describe the concept of generics in Go and common uses and misuses of generics.

Prerequisites

Following two points are pre-requisites to learn generics on a practical way :-

  • Go version 1.18 or greater installed.
  • A solid understanding of the Go language, such as variables, functions, struct types, loops and slices.

Concept of Generics

Consider the following function that extracts all the keys from a map[string]int type:

func getKeys(m map[string]int) []string {
	var keys []string
	for k := range m {
		keys = append(keys, k)
	}
	return keys
}

What if we want to use a similar feature for another map type such as a map[int]string? Before generics, Go developers had a few options: using code generation, reflection or duplicating code. For example, we could write two functions, one for each map type, or even try to extend getKeys to accept different map types.

//Accepts and returns any arguments
func getKeys(m any) ([]any, error) {
	var keys []any
	switch t := m.(type) {
	// Handles run-time errors if a type isn’t implemented yet
	default:
		return nil, fmt.Errorf("unknown type: %T", t)
	case map[string]int:
		for k := range t {
			keys = append(keys, k)
		}
		return keys, nil
	case map[int]string:
		for k := range t {
			keys = append(keys, k)
		}
		return keys, nil
	}
}

With this example, we start to notice a few issues:

  • It increases boilerplate code.
  • It requires duplicating the range loop.
  • It accepts any type which means Go is a Typed Language.
  • It checks whether a type is supported is done at run time instead of compile time.
  • It increases the effort on the caller side.
  • It returns an error if provided type is unknown so need to catch error.

With the use of generics, we can now refactor this code using type parameter are generic types that we can use with functions and types. For example following example accepts a type parameter.

//T is a Type parameter
func learn[T any](t T){
	///....
}	

When calling learn, we pass a type argument of any type. Supplying a type argument is
called instantiation, and the work is done at compile time. This keeps type safety as
part of the core language features and avoids run-time overhead.

Let’s get back to the getKeys function and use type parameters to write a generic
version that would accept any kind of map:

//The keys are comparable, whereas values are of the
//any type.
func getKeys[K comparable, V any](m map[K]V) []K {
	//Create the Key slices
	var keys []K
	for k := range m {
		keys = append(keys, k)
	}
	return keys
}

Common uses and misuses

When are generics useful? Let’s discuss a few common uses where generics are
recommended:

  • Data structure :- We can use generics to factor out the element type if we implement binary tree, stack, linked list.
  • Function working with slices,maps and channels of any type :- For example, a function to merge two channel would work with any channel type. Hence , we could use type parameters to factor out the channel type.
func merge[T any](ch1, ch2 <-chan T) <-chan T {
	// ...
}
  • Factoring out behaviors instead of types :- For example sort, search, etc.

Conversely, when is it recommended that we not use generics?

  • When calling a method of the type argument :- Consider a function that receives an io.Writer and call the Write method. For example:
//When calling a method of the type argument
func Dest[T io.Writer](w T) {
	b := getBytes()
	_, _ = w.Write(b)
}

In this case, using generics won’t bring any value to our code. We
should make the w argument an io.Writer directly.

  • When it make out code more complex :- Generics are never mandatory, and a Go developer, we have lived without them for more than a decade. If we are writing generic function or structures and we figure out that it doesn’t make our code clearer, we should probably reconsider our decision for that particular use case.

Finally, generic can be helpful in particular conditions, we should be cautious about when to use them and when not to use them. In general, if we want to answer when not to use generics, we can find similarities with when not to use interfaces. Indeed, generics introduce a form of abstraction, and we have to remember that unnecessary abstractions reflect complexity.

References:

  • Teiva Harsanyi, Go Mistakes and how to avoid them, Manning Publications Co
  • https://go.dev/blog/intro-generics
Previous Post
Next Post