Skip to main content

Generics

Outcomes

You will recognize when a type parameter removes duplication without erasing domain meaning.

Functions and types

func Map[A, B any](in []A, fn func(A) B) []B {
out := make([]B, 0, len(in))
for _, value := range in {
out = append(out, fn(value))
}
return out
}

type Page[T any] struct {
Items []T
Total int64
}

The compiler often infers type arguments from ordinary arguments. Constraints describe supported operations; use comparable for map keys or equality and a type set only when algorithms require operators.

Zero values

func First[T any](values []T) (T, bool) {
var zero T
if len(values) == 0 {
return zero, false
}
return values[0], true
}

Return an explicit boolean or error when the zero value is ambiguous.

Platform examples

  • platform/http.Handler[Req, Res] keeps handlers typed.
  • platform/paging.Page[T] provides one client paging shape.
  • platform/database/sql.Repo[T] scans and persists row types.
  • platform/events.Topic[T] binds event name, version, and payload type.
  • cache.GetOrLoad[T] returns typed cached values.
  • bootstrap.Spec[C] carries a typed service config.

Each generic solves one repeated mechanical pattern while preserving an explicit semantic boundary.

When not to use generics

Prefer an interface when implementations vary by behavior. Prefer a concrete domain function when two types merely look structurally similar. Avoid a generic repository that grows joins, workflows, validation, authorization, and auditing; those are different concerns.

Exercise

Implement Map and Filter. Then use paging.MapPage to convert storage rows to API responses while preserving metadata. Compare the code and decide which implementation belongs in application code.

Check yourself

  • How do constraints differ from interfaces used as values?
  • When does the compiler infer T?
  • Why does Page[T] help more than a result typed any?
  • What is the warning sign that a generic abstraction is swallowing domain behavior?