Generics, and when not to · 3 MIN READ

Most code that could be generic should not be

Recognise the three cases where a type parameter earns its place, and the ones where it does not.

The Go team's own guidance is narrow. Type parameters are for: functions over slices, maps and channels of any element type; general data structures like trees and stacks; and methods that are identical for several types apart from the type itself.

The case against everything else is readability. A generic function has to be read twice — once for what it does and once for what its constraint permits — and it cannot use anything the constraint does not promise, which usually makes it less capable than the concrete version it replaced.

Two smells worth naming. If the constraint ends up being an interface with methods and nothing else, an ordinary interface parameter is simpler and does the same thing at the cost of one dynamic dispatch. And if a function has a type parameter used exactly once, in one argument, it is a plain function wearing a costume.

SEE THE IDEA

Choose the abstraction that preserves the needed relationship

  1. 01

    Concrete type

    go
    func Print(v Record)

    Best when one type is enough

  2. 02

    Small interface

    go
    func Print(v Stringer)

    Best when only behavior matters

  3. 03

    Type parameter

    go
    func Clone[T any]([]T) []T

    Preserves the element type relationship

A type parameter should express a relationship the ordinary signature cannot express as clearly.

In practice

Start with the required behavior and the result type. If the caller needs the same concrete element type back, generics can carry that relationship. If the function only invokes one method and returns no type-related result, a small interface often communicates the contract more directly.

Watch out

Interface use does not guarantee a heap allocation, and generics do not guarantee faster code. Measure a real hot path first.

Official reference ↗

Read and predict

go
package main

import "fmt"

type Stringer interface {
	String() string
}

// Two ways to write the same thing.
func PrintGeneric[T Stringer](value T) { fmt.Println(value.String()) }
func PrintPlain(value Stringer)        { fmt.Println(value.String()) }

type ID int

func (i ID) String() string { return fmt.Sprintf("id-%d", i) }

func main() {
	PrintGeneric(ID(7))
	PrintPlain(ID(7))
}

Both compile and print the same thing. Which should you write, and why?

Try it yourself

Trace each value and write down your reasoning. Copy the source into GopherForge or your local Go environment to compile and check it. This web page does not execute your code or send it to a compiler service.

Progress stays in this browser. No account needed.