Day 13 — Generics: Type Parameters, Structs & Judgment
Day 13 — Generics: Type Parameters, Structs & Judgment
Day 31 — Generics intro
Stage IV · ~3h (theory-heavy)
Goal: Use type parameters and constraints confidently—write generic Map/Filter/Reduce helpers without abusing generics as a personality.
Generics (Go 1.18+) remove boilerplate, not design. Stage IV starts only after Stage III so you do not hide races behind type parameters.
Why this day exists
Before generics, common patterns needed:
interface{}/any+ type assertions
- Code generation
- Copy-paste with types renamed
Those still exist—but for pure algorithms over slices/maps/containers, type parameters are the right tool.
Theory 1 — Type parameters on functions
func Identity[T any](v T) T { return v }
x := Identity[int](3)
y := Identity("hi") // inference| Piece | Meaning |
|---|---|
[T any] |
Type parameter list; T constrained by any |
| Instantiation | Identity[int] or inferred from args |
any |
Alias for empty interface—unconstrained |
Multiple parameters
func Pair[A, B any](a A, b B) (A, B) { return a, b }Theory 2 — Constraints
A constraint is an interface limiting allowed type arguments.
func Min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}import "cmp" // Go 1.21+: cmp.Ordered
// or golang.org/x/exp/constraints historicallyGo 1.21+ prefers cmp.Ordered in many examples:
import "cmp"
func Min[T cmp.Ordered](a, b T) T {
if a < b {
return a
}
return b
}Inline interface constraints
type Number interface {
~int | ~int64 | ~float64
}
func Add[T Number](a, b T) T { return a + b }~ means underlying type: type MyInt int satisfies ~int.
Theory 3 — Approximation (~) and unions
type Stringy interface {
~string
}Union:
type Intish interface {
~int | ~int32 | ~int64
}Methods + types in one interface:
type Stringer interface {
~string
String() string // impossible? actually: type set + methods rules
}Know the rules at a high level: constraints define a type set. If unsure, keep constraints simple—any, cmp.Ordered, or small unions.
Theory 4 — Inference and explicit instantiation
Map([]int{1, 2}, func(x int) int { return x + 1 })
// compiler infers T, U
Map[int, string](xs, strconv.Itoa) // explicit when neededInference fails when arguments don’t pin types—then instantiate explicitly.
Theory 5 — Map / Filter / Reduce (stdlib-shaped)
Go did not put full FP combinators in stdlib early; slices and maps packages grew helpers. Still, writing your own teaches constraints.
func Map[T, U any](in []T, f func(T) U) []U {
out := make([]U, len(in))
for i, v := range in {
out[i] = f(v)
}
return out
}
func Filter[T any](in []T, keep func(T) bool) []T {
var out []T
for _, v := range in {
if keep(v) {
out = append(out, v)
}
}
return out
}
func Reduce[T, U any](in []T, init U, f func(U, T) U) U {
acc := init
for _, v := range in {
acc = f(acc, v)
}
return acc
}Prefer stdlib when available
import "slices"
slices.Sort(xs)
slices.Compact(xs)
// etc.Don’t reinvent a worse slices package for production—labs reinvent for learning.
Theory 6 — Generic interfaces (preview)
type Repo[T any] interface {
Get(id string) (T, error)
Save(T) error
}Useful; can also over-abstract. Day 32–33 drill judgment.
Theory 7 — What generics do not solve
- Performance magic (sometimes monomorphization helps; not why you start)
- Removing all interfaces
- Cross-package API churn without care
- Hiding concurrency mistakes
Worked example — keys of a map
func Keys[M ~map[K]V, K comparable, V any](m M) []K {
out := make([]K, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}comparable is required for map keys.
Worked example — pointer constraint pattern
func Zero[T any]() T {
var z T
return z
}Simple and useful for tests.
Lab 1 — Module + Map/Filter/Reduce
Workspace: ~/lab/90daysofx/01-go/day31
mkdir -p ~/lab/90daysofx/01-go/day31
cd ~/lab/90daysofx/01-go/day31
go mod init example.com/day31Implement Map, Filter, Reduce with tests:
go test -race ./...(Race optional here but keep the habit.)
Lab 2 — Min/Max with cmp.Ordered
func Min[T cmp.Ordered](a, b T) T
func Max[T cmp.Ordered](a, b T) T
func Clamp[T cmp.Ordered](v, lo, hi T) TTest with int, float64, string.
Lab 3 — ~ underlying types
type Celsius float64Write AddTemp[T ~float64](a, b T) T and show Celsius works.
Lab 4 — Replace interface{} code
Take a small pre-generic style helper (or write one) using any + assert; rewrite with type parameters. Compare error messages and call-site clarity.
Common gotchas
| Gotcha | Fix |
|---|---|
| Constraint too tight | Start with any, tighten |
| Constraint too loose | Compiler can’t use operators |
| Fighting inference | Explicit [T] |
| Generic everything | Day 33—push back |
Ignoring slices/maps |
Prefer stdlib in real code |
| Method receivers on generic funcs | Methods come Day 32 types |
Checkpoint
- Writes generic functions with inference
- Uses
cmp.Orderedor union constraints
- Explains
~
- Ships Map/Filter/Reduce tests
- Knows generics ≠ free performance
go testgreen
Commit
git add .
git commit -m "day31: generics intro — Map/Filter/Reduce + constraints"Write three personal gotchas before continuing.
Tomorrow
Day 32 — Generic structs & self-referential constraints: type-parameterized data structures and Go 1.26 self-referential type parameter notes.
Day 32 — Generic structs
Stage IV · ~3h (theory-heavy)
Goal: Design generic types (structs/interfaces) with methods, understand instantiation at the type level, and apply Go 1.26 self-referential type parameter constraints where they clarify APIs.
Generic data structures are where type parameters pay rent—safe stacks, result types, keyed sets—when the element type is truly parametric.
Why this day exists
Function generics help algorithms. Type generics help models:
Stack[T]
Result[T]
Set[T comparable]
- Graph nodes with self-referential constraints (1.26+)
Without care you invent Java-enterprise type soup. With care you delete unsafe any.
Theory 1 — Generic struct declaration
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
i := len(s.items) - 1
v := s.items[i]
s.items = s.items[:i]
return v, true
}Notes:
- Methods declare
[T]on the receiver type name, not a second parameter list of free params (methods cannot introduce new type params in Go).
- Pointer vs value receiver rules unchanged.
- Zero value
Stack[T]{}is ready to use if fields are.
Theory 2 — Instantiation
var si Stack[int]
ss := Stack[string]{}
sp := &Stack[Person]{}You cannot have a variable of type Stack without type args—partial types aren’t values.
Type aliases:
type IntStack = Stack[int]Theory 3 — Constraints on type parameters of types
type Set[T comparable] struct {
m map[T]struct{}
}
func NewSet[T comparable]() *Set[T] {
return &Set[T]{m: make(map[T]struct{})}
}
func (s *Set[T]) Add(v T) { s.m[v] = struct{}{} }
func (s *Set[T]) Has(v T) bool { _, ok := s.m[v]; return ok }comparable is mandatory for map keys.
Theory 4 — Generic interfaces
type Queue[T any] interface {
Enqueue(T)
Dequeue() (T, bool)
}
type SliceQueue[T any] struct{ s []T }
func (q *SliceQueue[T]) Enqueue(v T) { q.s = append(q.s, v) }
func (q *SliceQueue[T]) Dequeue() (T, bool) { /* ... */ }Interface satisfaction remains implicit—at instantiated types.
Theory 5 — Go 1.26 self-referential type parameters
Go 1.26 lifts a restriction: a generic type may refer to itself in its type parameter list/constraints.
type Adder[A Adder[A]] interface {
Add(A) A
}
func Sum[A Adder[A]](x, y A) A {
return x.Add(y)
}Why it matters
Previously, patterns like “type must be able to combine with itself” needed awkward workarounds. Self-referential constraints enable cleaner abstract-algebra-style APIs and some recursive structure definitions.
Practical guidance
- Use when the constraint truly is self-referential
- Don’t force every model into
Node[N Node[N]]fashion statements
- Prefer simple
any/comparableuntil a self-ref constraint removes real duplication
Example sketch—ordered merge of like types:
type Merger[T Merger[T]] interface {
Merge(T) T
}Theory 6 — Pointer method sets and generics
If constraint requires a method with pointer receiver, the type argument may need to be *T:
type Stringer interface {
String() string
}
func Print[T Stringer](v T) { fmt.Println(v.String()) }
// If String has pointer receiver on Foo, use Print[Foo] only if value methods include String;
// otherwise Print[*Foo](&foo).Same method-set rules as non-generic Go.
Theory 7 — Embedding and composition
type Pool[T any] struct {
Stack[T]
mu sync.Mutex
}
func (p *Pool[T]) Get() (T, bool) {
p.mu.Lock()
defer p.mu.Unlock()
return p.Pop()
}Mutex still must not be copied; use pointers to pools.
Worked example — Result[T]
type Result[T any] struct {
Val T
Err error
}
func OK[T any](v T) Result[T] { return Result[T]{Val: v} }
func Err[T any](err error) Result[T] { return Result[T]{Err: err} }
func (r Result[T]) Unwrap() (T, error) {
return r.Val, r.Err
}Useful for channel payloads; still idiomatic to use (T, error) at exported sync APIs.
Worked example — linked list node
type Node[T any] struct {
Val T
Next *Node[T]
}
func (n *Node[T]) Len() int {
c := 0
for n != nil {
c++
n = n.Next
}
return c
}Lab 1 — Generic Stack + tests
Workspace: ~/lab/90daysofx/01-go/day32
mkdir -p ~/lab/90daysofx/01-go/day32 && cd ~/lab/90daysofx/01-go/day32
go mod init example.com/day32Implement Stack[T] with Push/Pop/Peek/Len. Table tests for int and string.
go test -race ./...Lab 2 — Set and Map helpers
Set[T comparable] with Add/Remove/Has/Len/Elements.
Optional: MapKeys[K comparable, V any](map[K]V) []K.
Lab 3 — Self-referential constraint (1.26)
On Go 1.26.x:
- Define a small interface with self-ref type parameter (like
Adder).
- Implement for a concrete type (
IntwithAdd).
- Write a generic algorithm using the constraint.
- Note in journal how this differs from Go ≤1.25 restriction.
If tooling blocks, write the code and comment compile requirements.
Lab 4 — Thread-safe pool
Combine Day 22 mutex with Stack[T] as a free-list pool. Tests under -race with parallel alloc/free.
Common gotchas
| Gotcha | Fix |
|---|---|
| Methods adding new type params | Not allowed—put on package funcs |
| Forgetting comparable for map keys | Constrain T comparable |
| Copying mutex in generic struct | Pointer receivers; no value copy |
| Over-abstract self-ref constraints | Use only when needed (Day 33) |
| Exported type with awkward inference | Provide constructor NewFoo[T](...) |
Checkpoint
- Built
Stack[T]andSet[T]
- Explains type instantiation
- Understands methods cannot introduce new type params
- Tried Go 1.26 self-referential constraint example
- Race-clean concurrent pool optional lab
go testgreen
Commit
git add .
git commit -m "day32: generic structs — Stack/Set + 1.26 self-ref notes"Write three personal gotchas before continuing.
Tomorrow
Day 33 — When not to use generics: remove needless type parameters, prefer interfaces/stdlib, and practice judgment under review pressure.
Day 33 — When not to use generics
Stage IV · ~3h (theory-heavy)
Goal: Develop judgment—spot needless type parameters, refactor them out, and know when interfaces, any, code generation, or plain duplication is the better design.
Generics are a power tool. Stage IV fails if every function grows [T any]. Expert Go still looks mostly non-generic at application boundaries.
Why this day exists
After Days 31–32, the temptation is:
- Generic repository of generic repository of…
- Public APIs that force callers to write type args everywhere
- Constraints nobody can read
- “Type-safe” wrappers that only ever instantiate one type
This day is the antidote: delete type parameters that don’t earn their complexity. Judgment is a skill you will reuse every time you review a PR.
Theory 1 — When generics are justified
Prefer type parameters when:
- Identical algorithms over many types (containers, slices helpers)
- Compiler-enforced element safety beats runtime asserts
- Zero/allocation patterns shared without interface boxing pain
- You would otherwise generate code or copy nearly identical files
- The abstraction is stable (containers age well; domain workflows less so)
Examples: slices.SortFunc, your Stack[T], graph algorithms over node IDs, small functional helpers like Map/Filter in internal packages.
// Justified: one algorithm, many element types, zero domain coupling.
func First[T any](s []T, pred func(T) bool) (T, bool) {
var zero T
for _, v := range s {
if pred(v) {
return v, true
}
}
return zero, false
}Theory 2 — When generics are not justified
Avoid or remove generics when:
| Smell | Prefer |
|---|---|
| Only one type argument ever used | Concrete type |
Constraint is just any and body uses reflection |
Redesign |
| API is about behavior, not representation | Interface methods |
| Type param only threads through without use | Delete it |
| Call sites become unreadable | Split or concrete |
| Performance unmeasured “because monomorphization” | Measure first |
Domain nouns (Invoice[T], UserService[TRepo]) |
Interfaces + composition |
Interface-first domain models
type Store interface {
Get(ctx context.Context, id string) (Item, error)
Create(ctx context.Context, it Item) error
}Often better than:
type Store[T any] interface {
Get(ctx context.Context, id string) (T, error)
Create(ctx context.Context, v T) error
}unless you truly have many stored entity shapes sharing the same package algorithmically (rare in application code).
The “one instantiation forever” test
If go test and your binary only ever use Store[User], the type parameter is documentation theater. Collapse to UserStore.
Theory 3 — Generics vs any vs assertions
| Tool | Cost | Benefit |
|---|---|---|
| Generics | API complexity, inference noise | Compile-time safety |
any |
Runtime assert risk | Flexibility |
| Interfaces | Small method sets | Polymorphism by behavior |
| Duplication | Lines of code | Clarity for 2 cases |
Two copies can beat one generic if the types diverge next week. Duplication is reversible; a wrong abstraction is sticky.
// Runtime escape hatch — only at boundaries you control.
func asString(v any) (string, error) {
s, ok := v.(string)
if !ok {
return "", fmt.Errorf("want string, got %T", v)
}
return s, nil
}Do not rebuild half of generics with any + panic-on-assert.
Theory 4 — Premature abstraction timeline
- Write concrete code
- Write a second concrete case
- Feel real pain (copy-paste bugs, divergent fixes)
- Abstract (generic or interface)
Jumping to step 4 at step 1 is how libraries rot. The two-case rule is not dogma—it is a default delay that forces evidence.
Day 1: UserRepo with concrete methods
Day 14: ProductRepo looks 90% identical
Day 15: extract shared helpers OR Store[T] if Scan/shape truly match
Theory 5 — Refactoring patterns that remove generics
Collapse single-use param
// before
func Print[T any](v T) { fmt.Println(v) }
// after
func Print(v any) { fmt.Println(v) } // or just fmt.Println at call siteReplace with interface
// before
func SaveAll[T Entity](xs []T) error
// after — if Entity is the behavior you need
func SaveAll(xs []Entity) errorBound at the edges
Keep generics inside a package; export concrete wrappers:
func NewUserStack() *Stack[User] { return &Stack[User]{} }Callers never type-arg. Your public surface stays boring—on purpose.
Delete pass-through params
// SMELL: T never appears in a meaningful constraint or algorithm
func (s *Service[T]) Run(ctx context.Context) error {
return s.inner.Run(ctx)
}
// after: Service without T; compose inner directlyTheory 6 — Readability budget
Every type parameter costs:
- Cognitive load at definition
- Inference failures at call site
- Worse godoc for beginners
- Harder stack traces and IDE hover walls
Budget: if the function is domain business logic (billing, auth, order workflow), generics are rarely first choice. If it is a container/algorithm, generics shine.
// Hard to teach, hard to call
func Pipe[A, B, C any](a A, f func(A) B, g func(B) C) C { return g(f(a)) }
// Clear at the call site
b := f(a)
c := g(b)Worked example — over-generic API
// SMELL: three free type params, no shared package story
func Handle[T any, U any, V any](
ctx context.Context,
in T,
mapFn func(T) (U, error),
reduceFn func(V, U) V,
init V,
) (V, error) {
u, err := mapFn(in)
if err != nil {
return init, err
}
return reduceFn(init, u), nil
}Nobody enjoys calling this. Split steps:
u, err := mapFn(in)
if err != nil {
return err
}
out := reduce(init, u)Or accept []U after a normal loop. Name the domain operations.
Worked example — justified generic
// GOOD: clear container, one concept, obvious methods
type Ring[T any] struct {
buf []T
head int
full bool
}
func NewRing[T any](n int) *Ring[T] {
if n <= 0 {
panic("ring size must be positive")
}
return &Ring[T]{buf: make([]T, n)}
}
func (r *Ring[T]) Push(v T) {
r.buf[r.head] = v
r.head = (r.head + 1) % len(r.buf)
if r.head == 0 {
r.full = true
}
}One concept, one type param, methods that need T. This is the gold standard shape.
Worked example — interface wins for domain
package billing
type Pricer interface {
Price(ctx context.Context, sku string, qty int) (Money, error)
}
type Checkout struct {
prices Pricer
}
func (c *Checkout) Total(ctx context.Context, lines []Line) (Money, error) {
var sum Money
for _, ln := range lines {
p, err := c.prices.Price(ctx, ln.SKU, ln.Qty)
if err != nil {
return Money{}, err
}
sum = sum.Add(p)
}
return sum, nil
}Making Checkout[P Pricer] gains nothing: the method set already expresses variation.
Lab 1 — Find and kill
Workspace: ~/lab/90daysofx/01-go/day33
- Revisit Day 31–32 code (or write a deliberately over-generic module).
- Identify ≥3 type parameters that don’t earn keep.
- Refactor them out.
- Keep tests green.
mkdir -p ~/lab/90daysofx/01-go/day33
cd ~/lab/90daysofx/01-go/day33
go mod init example.com/day33
go test -race ./...Journal: for each removal, one sentence “why it failed the keep test.”
Lab 2 — Two-case rule
Implement sorting of []User by name without generics:
slices.SortFunc(users, func(a, b User) int {
return cmp.Compare(a.Name, b.Name)
})Then add []Product by price. Only if the comparison logic is truly shared beyond the callback, introduce a generic helper—not a giant generic domain type.
Journal the decision: concrete + SortFunc vs SortBy[T].
Lab 3 — Interface vs generic store
Write both:
UserStoreinterface + memory backend
Store[T]generic + memory backend
Implement in a tiny main that does Get/Put for users. Compare:
| Axis | UserStore | Store[T] |
|---|---|---|
| Call-site clarity | ||
| Test ergonomics | ||
Adding Order entity |
Choose a winner for this program and defend it in 5 sentences in NOTES.md.
Lab 4 — Review checklist
Create review-generics.md:
- Each type param named meaningfully (
Tok for pure containers)
- Constraints minimal (no kitchen-sink interfaces)
- No free
anyparams that only pass through
- Exported API readable without a blog post
- Stdlib alternative considered (
slices,maps,cmp)
- At least two real instantiations or concrete collapse
Apply the checklist to Day 31–32 packages; fix findings; re-run tests.
Lab 5 — Readability rewrite
Take one exported function with ≥2 type parameters. Rewrite it as:
- Concrete types, or
- Interface methods, or
- Smaller generic helper used internally only
Commit the before/after as comments or a short before.go.txt snippet in the lab notes.
Common gotchas
| Gotcha | Fix |
|---|---|
| Generic for one type | Concrete |
| Interface + generic redundancy | Pick one axis of variation |
Exporting raw Stack[T] everywhere |
Constructors / aliases |
| Using generics to avoid designing methods | Model behavior with interfaces |
| Copy-paste of Java/C# patterns | Write Go |
~string constraints “just in case” |
Add when a second type needs them |
Public any constraints that panic inside |
Validate at edges; prefer real constraints |
Checkpoint
- Removed needless generics in a real package
- Articulated two-case rule with an example
- Compared interface vs generic store in writing
- Has a reusable review checklist
- Tests still pass under
-race
- Can explain “when not to” without dogma
Commit
git add .
git commit -m "day33: generics judgment — remove needless type params"Write three personal gotchas before continuing.
Tomorrow
Day 34 — Fuzzing: go test -fuzz to attack parsers and find inputs your tables never imagined.