Day 32 — Generic structs

Updated

July 30, 2026

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.

Note

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/comparable until 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/day32

Implement 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:

  1. Define a small interface with self-ref type parameter (like Adder).
  2. Implement for a concrete type (Int with Add).
  3. Write a generic algorithm using the constraint.
  4. 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] and Set[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 test green

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.