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
- 01
Concrete type
gofunc Print(v Record)Best when one type is enough
- 02
Small interface
gofunc Print(v Stringer)Best when only behavior matters
- 03
Type parameter
gofunc Clone[T any]([]T) []TPreserves 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.
Read and predict
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?
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.