Day 32 — Generic structs
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.