Idiomatic Generics
Overview
Generics are powerful but should be used judiciously. This chapter covers when to use generics vs interfaces, and best practices.
When to Use Generics
✅ Good uses: - Container types (Stack, Set, Queue) - Utility functions (Map, Filter, Reduce) - Type-safe concurrent constructs - Avoiding reflection
❌ Don’t use when: - Interface works fine - Only one type will ever be used - Adding complexity without benefit
Generics vs Interfaces
Use Interface When:
// Behavior-based polymorphism
type Writer interface {
Write([]byte) (int, error)
}
func Save(w Writer, data []byte) error {
_, err := w.Write(data)
return err
}Use Generics When:
// Type preservation needed
func Reverse[T any](s []T) []T {
result := make([]T, len(s))
for i, v := range s {
result[len(s)-1-i] = v
}
return result
}Guidelines
1. Start with Concrete Types
// Start here
func ReverseInts(s []int) []int { }
// Generalize only when needed
func Reverse[T any](s []T) []T { }2. Use Meaningful Constraints
// Too broad
func Process[T any](v T) { }
// Better: documents requirements
func Process[T constraints.Ordered](v T) { }3. Don’t Over-Constrain
// Over-constrained
type Number interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64
}
// Better: use existing constraint
func Sum[T constraints.Integer | constraints.Float](nums []T) T4. Type Inference is Your Friend
// Let Go infer when obvious
result := Map(nums, double)
// Explicit only when needed
result := Convert[int, float64](nums)Common Patterns
slices Package (stdlib)
import "slices"
slices.Sort(nums)
slices.Reverse(nums)
slices.Contains(nums, 5)
slices.Index(nums, 5)maps Package (stdlib)
import "maps"
maps.Clone(m)
maps.Keys(m)
maps.Values(m)Summary
| Decision | Choice |
|---|---|
| Need type preservation | Generics |
| Need polymorphic behavior | Interface |
| Operating on containers | Generics |
| Method set requirements | Interface |
More examples
Example: prefer clear non-generic API when one type
Save as main.go and go run . (with go mod init example if needed).
package main
import "fmt"
// SumInts is clearer than Sum[int] when you only ever sum ints.
func SumInts(nums []int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(SumInts([]int{1, 2, 3}))
fmt.Println(SumInts(nil))
}Expected:
6
0
Example: constrain only what you need
Save as main.go and go run . (with go mod init example if needed).
package main
import (
"fmt"
"strconv"
)
// Stringer-like constraint without importing fmt for the type set.
type stringer interface {
String() string
}
type ID int
func (id ID) String() string { return "id=" + strconv.Itoa(int(id)) }
func PrintAll[T stringer](items []T) {
for _, it := range items {
fmt.Println(it.String())
}
}
func main() {
PrintAll([]ID{7, 8})
}Expected:
id=7
id=8
Runnable example
Save as main.go. Then:
go mod init example
go run .package main
import (
"fmt"
"io"
"maps"
"slices"
"strings"
)
// Behavior polymorphism → interface (not generics).
func Save(w io.Writer, data []byte) error {
_, err := w.Write(data)
return err
}
// Type preservation on containers → generics.
func Reverse[T any](s []T) []T {
out := make([]T, len(s))
for i, v := range s {
out[len(s)-1-i] = v
}
return out
}
func main() {
var b strings.Builder
_ = Save(&b, []byte("hello"))
fmt.Println("interface writer:", b.String())
nums := []int{1, 2, 3}
fmt.Println("Reverse ints:", Reverse(nums))
fmt.Println("original unchanged:", nums)
words := []string{"go", "is", "fun"}
fmt.Println("Reverse strings:", Reverse(words))
// Prefer stdlib generic helpers when they fit.
cloned := slices.Clone(nums)
slices.Sort(cloned)
fmt.Println("slices.Sort clone:", cloned)
fmt.Println("slices.Contains:", slices.Contains(words, "is"))
m := map[string]int{"b": 2, "a": 1}
cp := maps.Clone(m)
cp["c"] = 3
fmt.Println("maps.Clone size:", len(m), "->", len(cp))
}Expected output:
interface writer: hello
Reverse ints: [3 2 1]
original unchanged: [1 2 3]
Reverse strings: [fun is go]
slices.Sort clone: [1 2 3]
slices.Contains: true
maps.Clone size: 2 -> 3
What to notice: Save cares about behavior (Write), so an interface is enough. Reverse must preserve element type across many containers, so generics win. Reach for slices / maps before inventing another utility package.
Try next: Start from a concrete ReverseInts and only generalize when a second type appears; replace a hand-rolled Contains with slices.Contains.