Skip to content

Day 023 — Generics Fundamentals

Month 1 · Week 4 · ⬅ Day 022 · Day 024 ➡ · Journal index

🎯 Learning Objective

Write generic functions and types with type parameters and constraints, and know when generics help vs. when an interface is the better tool.

📚 Topics

  • Type parameters [T any], multiple params [K comparable, V any]
  • Constraints: any, comparable, the cmp.Ordered interface, custom union constraints (~int | ~float64)
  • Type inference vs explicit instantiation
  • When NOT to use generics (interfaces vs. type params)

📖 Reading / Sources

📝 Notes

  • A type parameter is a placeholder type listed in square brackets before the ordinary params: func Index[T comparable](s []T, v T) int. T is bound at the call site by inference or explicit instantiation Index[string](...).
  • A constraint is just an interface used in a type-parameter list. It restricts which types may be substituted and which operations are legal inside the body.
  • any = the empty interface interface{}: no operations beyond assignment/passing.
  • comparable = types usable with ==/!= (needed for map keys, set membership). Links to [[maps-sets]]. NB: comparable does not mean ordered.
  • cmp.Ordered (stdlib, Go 1.21+) = types supporting < <= >= > — ints, floats, strings. Enables cmp.Less, min/max over generics.
  • Union / approximation constraints: ~int | ~int64 | ~float64 permits those types and any named type whose underlying type is one of them (the ~ means "underlying type"). This is how you write a numeric Sum.
  • Type inference: the compiler infers T from the argument types, so callers usually omit [...]. You must instantiate explicitly when inference can't see the type (e.g. Make[int]() with no args determining it).
  • Built-in min/max/clear: Go 1.21 added min, max (variadic, work on any ordered type) and clear (maps/slices) as builtins — often you don't need a custom generic at all.
  • When NOT to use generics: if behavior differs per type, use an interface (dynamic dispatch). Reach for generics when the algorithm is identical and you only want to avoid interface{} boxing and type assertions (containers, map/filter/reduce). "If you're writing the same code for []int and []string, generalize; if you're switching on type, use an interface."
  • Constraints can't be used as ordinary types: you can't write var x cmp.Ordered. They only appear in type-parameter lists.

💻 Code Examples

import "cmp"

// One algorithm, any ordered element type.
func Max[T cmp.Ordered](s []T) (T, bool) {
    var zero T
    if len(s) == 0 {
        return zero, false // empty: report not-ok rather than panic
    }
    m := s[0]
    for _, v := range s[1:] {
        if v > m { // legal because cmp.Ordered guarantees `>`
            m = v
        }
    }
    return m, true
}

// Union constraint enables `+` on numeric types (incl. named types via ~).
type Number interface{ ~int | ~int64 | ~float64 }

func Sum[T Number](s []T) T {
    var total T // zero value of the element type
    for _, v := range s {
        total += v
    }
    return total
}

Full code: examples/month-01/generics/main.go · Run: go run ./examples/month-01/generics

🏋️ Exercises / Practice

Exercise Status Link
Generic Map/Filter/Reduce/Sum/Keys exercises/month-01/week-4/genslice

🐛 Mistakes Made

  • Used comparable and then tried a < b inside the body → compile error. comparable only grants ==/!=; I switched the constraint to cmp.Ordered.
  • Forgot the ~ in a union constraint, so a named type type Celsius float64 was rejected. ~float64 (approximation) accepts it.

❓ Open Questions

  • Performance: does the compiler monomorphize or use dictionaries (GCShape stenciling)? When does that cost matter? (Parked — measure before worrying.)

🧠 Active Recall (answer without looking)

  1. Q: What's the difference between comparable and cmp.Ordered?

    A `comparable` allows `==`/`!=` only (map keys, set membership). `cmp.Ordered` additionally allows the ordering operators `< <= >= >` (ints, floats, strings).

  2. Q: What does the ~ in a constraint like ~int mean?

    A "Any type whose **underlying type** is `int`" — so it matches `int` and named types such as `type Age int`, not just `int` itself.

🪶 Feynman Reflection

Generics let me write an algorithm once and have the compiler stamp out a correct, type-safe version per element type — no interface{} boxing, no runtime type assertions. The constraint is the contract: it tells both the compiler and me which operations the placeholder type is allowed to do.

🕳️ Knowledge Gaps

  • Designing my own constraint interfaces (with methods + type sets together) — I'll try it building the generic container tomorrow.

✅ Summary

I can declare type parameters, pick the right constraint (any / comparable / cmp.Ordered / unions with ~), rely on inference, and choose generics vs interfaces deliberately.

⏭️ Next Steps / Prep for Tomorrow

  • Day 024: build generic data structures — a Stack[T] and a Set[T].

Time spent Difficulty Confidence
95 min 🟦🟦🟦⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: feat(examples): generics fundamentals — type params and constraints (day 023)