DEFAULT
Go — tagged struct
presence is the tag — no separate discriminant field
type Circle struct{ R float64 } type Rect struct{ W, H float64 } type Shape struct { Circle *Circle `json:"circle,omitempty"` Rect *Rect `json:"rect,omitempty"` } func (s Shape) Area() float64 { a := 0.0 if s.Circle != nil { a += math.Pi*s.Circle.R*s.Circle.R } if s.Rect != nil { a += s.Rect.W*s.Rect.H } return a }
Go already ships a sum type. Nothing enforces it.
return 42, errors.New("boom") // both set, compiles
return 0, nil // both zero, compiles
(T, error) is Go’s most-used two-variant sum type. No type stops either case — the whole ecosystem runs on “check err first,” and it works.None set → 0. Both set → summed. Neither is a bug — same shape as search filters, PATCH bodies, config, where “none set” is legal too. Nothing to be exhaustive over.
Presence is the tag — no separate discriminant to disagree with the payload. That’s structural here, not something a constructor has to maintain. Honest cost: a pointer indirection and a heap allocation per variant that’s actually set.
NARROW CASE
Go — sealed interface
when exactly-one is safety-critical
type Shape interface{ shape() } switch s := s.(type) { case Circle: ... case Rect: ... }
✓ Tag = itab pointer, set by the compiler when boxed — can’t disagree with the payload. The invariant is free and unbreakable.
! Costs a 2-word interface value and a heap escape for the payload. Still no exhaustiveness check — a missed case just compiles.
Earns its keep when “exactly one” is load-bearing:
protocol decoders — a byte on the wire selects the type
state machines — the state determines the payload shape
ledger / billing records — mixing fields is a real bug, not a style nit
Want both guarantees anyway? Unexport the variant fields, add constructors (
OfCircle, OfRect) — the same trick as this interface: an unexported identifier behind a package boundary. Pair with a Match(onCircle, onRect) func, one handler per variant, and a missing handler stops every call site compiling. Usually not worth the ceremony — the bill either way: no early return from a handler, closures may allocate, JSON needs a hand-written boundary type.