For a type to be a subtype of another type, we need to be able to use it in exactly the same way everywhere we use the supertype. The Liskov Substitution Principle explains the mechanics for this, which I find easier to grasp through examples.
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
type SortFunc func(sort.Interface)
This function type can sort anything implementing the Interface. We could back it with QuickSort, MergeSort, or BubbleSort. But RadixSort subtly violates the contract: it only works with fixed-size strings. Being more restrictive than the supertype’s expectations, RadixSort cannot be a drop-in replacement and thus isn’t a true subtype.
Go’s type system can’t express this constraint, so RadixSort’s signature looks identical to SortFunc. The compiler won’t catch the violation, inviting runtime panics or undefined behavior. Languages with dependent types like Idris or Agda can encode such constraints directly (’this string has fixed length’), though some invariants may be impossible to model in any type system.
Conversely, a subtype with more relaxed constraints poses no problem. An algorithm that sorts all integers can safely back an abstraction expecting only positive numbers.
These input constraints are preconditions. We encode them in types where possible; what remains gets documented in comments and enforced through compliance tests.
The flip side is postconditions, the guarantees we provide. A sort function guarantees a sorted list, but implementations are free to offer more: no extra allocations, stability, specific time complexity.
The asymmetry in LSP is this: subtypes can weaken preconditions making them easier to call, while they can strengthen postconditions.