Go and Rust shipped without implementation inheritance at all. What replaces it, what does a team actually pay in code that would have used a three-level class hierarchy, and what does the omission buy?
answer
- Rust: traits + default bodies (re-dispatch works) + generics or dyn; no fields in traits
- Go: embedding promotes state and methods; interfaces implicit, defined by the consumer
- Shared behaviour ports; shared state costs forwarders
- Deref-as-inheritance and the orphan rule are the pressure points
- Expression problem: new operations easy with traits, hard in a hierarchy
basics
~20 sRust replaces it with trait default methods plus generics or dyn Trait; Go with struct embedding plus implicitly satisfied interfaces. Both share behaviour but not state, so teams pay forwarding boilerplate and lose subclass hooks. They buy no fragile base coupling, no diamond, and easier addition of new operations.
solid answer
~60 s**What replaces it.** Rust: traits with default method bodies (which do re-dispatch through `self`, so hooks work), generics that monomorphise, and `dyn Trait` for runtime polymorphism. Go: embedding promotes methods, and interfaces are satisfied structurally and implicitly, so an abstract base becomes an interface plus a shared struct you embed. **What you pay.** Neither shares *state* the way a base class does: Rust traits cannot hold fields, so shared data is reached through required accessors or held in a struct you forward to -- which is why crates like `delegate` and `ambassador` exist. Go embedding has no re-entry into the outer type, so subclass-hook frameworks do not translate; you inject an interface instead. Deep abstract-base designs get flattened, not ported. **What it buys.** No base whose internal call pattern silently constrains you, no diamond, no accidental capture as a base grows, and the expression problem attacked from the other side: adding a new *operation* over existing types is a new trait, not an edit to every class. Rust pays for that with the orphan rule.
code
rust · 8 linestrait Report {
fn rows(&self) -> usize; // required: the varying step
fn title(&self) -> &str; // shared STATE must come through an accessor
fn render(&self) -> String { // shared BEHAVIOUR, re-dispatches via self
format!("{} ({} rows)", self.title(), self.rows())
}
}go deeper
Know that some mainstream languages have no base classes and rely on interfaces or traits plus embedding for reuse.
Explain the substitutes concretely: trait default methods, generics versus dyn Trait, Go embedding, implicitly satisfied interfaces.
Separate shared behaviour (ports cleanly) from shared state (costs forwarders), and name the real pressure points -- delegation macros, the Deref anti-pattern, the orphan rule.
Frame it as unbundling: inheritance shipped subtyping, code reuse, state reuse and open recursion together, and these languages kept the composable pieces. Tie the trade to the expression problem and to how much of your design is state versus behaviour.
## The bet Two widely used systems languages designed in the last two decades looked at implementation inheritance and declined it. Go has structs, interfaces and embedding; Rust has structs, enums and traits. Neither has a base class. That is a strong claim about the reuse-only inheritance critique: the argument was not "use it carefully", it was "the mechanism is not worth its coupling". ## What actually replaces each thing a hierarchy did A three-level hierarchy typically does four jobs at once, and each has a separate replacement. **Polymorphism.** Rust: a trait plus `dyn Trait` behind a pointer for runtime dispatch, or a generic parameter that monomorphises to a direct call at compile time. Go: an interface, satisfied implicitly -- the type never declares that it implements anything, so the interface can be defined by the *consumer*, at the point of use, rather than by the provider. That inverts a real dependency: in a class hierarchy the abstraction lives with the supplier and every consumer is coupled to it. **Shared behaviour.** Rust: default method bodies on a trait. These are more capable than they first look, because a default body may call the trait's required methods through `self`, and those calls resolve to the implementing type -- so Template Method-style hooks work fine. Go: embedding a struct promotes its methods, which covers straightforward code reuse but has no re-dispatch back into the outer type, so hook-shaped designs must be inverted into an injected interface. **Shared state.** This is where the omission bites. Rust traits cannot declare fields at all. If three implementers need the same three fields, you either declare required accessor methods in the trait and have each implementer write them, or you factor the fields into a struct and give each type a member plus forwarding methods. Go embedding does share state -- an embedded struct's fields are promoted -- so Go handles this case more gracefully than Rust. **Type identity / exhaustiveness.** A sealed class hierarchy answers "what are all the cases". Rust replaces it with `enum` plus exhaustive `match`, which is a better fit for closed sets and is checked by the compiler. Go has no equivalent and leans on interfaces plus, occasionally, type switches with a default branch. ## What a team pays - **Forwarding boilerplate.** The single most common complaint. When you cannot inherit state and behaviour together, you write delegating methods. Rust's ecosystem responded with macro crates (`delegate`, `ambassador`) whose whole purpose is generating forwarders, which is evidence that the cost is real and recurring rather than theoretical. - **Framework styles do not port.** Anything built on "extend our base class and override these three protected methods" has no direct translation. Both languages push registration and injection instead: pass a function or an interface value. Teams migrating a subclass-hook design must redesign, not translate. - **The Deref temptation.** Rust newcomers discover that implementing `Deref` makes a wrapper's inner methods appear on the wrapper, which mimics inheritance. It is explicitly discouraged: `Deref` is for smart pointers, and abusing it produces surprising method resolution and poor error messages. That a widely warned-against anti-pattern exists is a fair measure of the pressure the omission creates. - **The orphan rule.** Rust's coherence rule -- you may implement a trait for a type only if you own the trait or the type -- is the price of guaranteeing a single implementation exists globally. It blocks the natural move of "implement someone else's trait for someone else's type" and forces newtype wrappers, which is more forwarding. ## What it buys - **No coupling to a base's internal call pattern.** In Go, an embedded type cannot re-enter the outer type, so changing which internal calls it makes cannot alter an embedder's behaviour. That deletes a whole failure category rather than mitigating it. - **No diamond, no linearization, no accidental capture.** There is no ancestor chain whose order is part of the API, and a method added to an embedded or implemented type cannot silently capture a call that was going somewhere else -- in Go an ambiguous promotion is a compile error at the use site. - **The expression problem from the other side.** In a class hierarchy, adding a new *type* is easy (write a subclass) and adding a new *operation* is hard (edit every class, or introduce a visitor). With Rust traits, adding a new operation over existing types is easy -- define a trait and write impls -- and adding a new type is also easy. The orphan rule is precisely the constraint that keeps this from being free. - **Predictable cost.** Rust's generics monomorphise to static calls; you opt into dynamic dispatch by writing `dyn`. Go's interface calls are indirect but there is no hierarchy depth to traverse. ## The judgement The honest summary is that these languages did not remove reuse; they removed the *bundling*. Inheritance offered subtyping, code reuse, state reuse and open recursion in one keyword, and both languages unbundled it and shipped the pieces separately -- keeping the ones that compose (interfaces, embedding, trait defaults) and dropping the one that couples hardest (inherited state plus overridable internals). For a team, the practical test is how much of your design is shared *state* versus shared *behaviour*. Shared behaviour ports cleanly. Shared state is where you will write forwarders and where you should expect the port to be a redesign.
- Rust's orphan rule blocks implementing a foreign trait for a foreign type. What is it protecting, and what does it cost?It protects coherence: the guarantee that for any (trait, type) pair there is at most one implementation in the whole program, so behaviour cannot change depending on which crates happen to be linked. The cost is that the natural extension move is unavailable and you must define a newtype wrapper around the foreign type, then forward every method you need -- more of the same boilerplate the missing inheritance already causes.
- Why is implementing Deref to mimic inheritance discouraged in Rust?Deref exists to express smart-pointer relationships, and method resolution follows deref coercions, so abusing it makes an unrelated type's whole method set appear on your wrapper -- the same API pollution that reuse-only inheritance causes, plus confusing errors and surprising resolution when names collide. The idiomatic alternative is explicit forwarding, generated by a macro if it is tedious.
- Go interfaces are satisfied implicitly. How does that change where abstractions live compared with a class hierarchy?In a hierarchy the abstraction is declared by the supplier -- the base class or interface ships with the library and every consumer depends on it. In Go the consumer declares the interface it needs, listing only the methods it calls, and any existing type satisfies it without modification. Abstractions therefore end up small and located at the point of use, which reduces coupling and makes test doubles trivial.
Inheritance is a package holiday -- flights, hotel and transfers in one purchase. Go and Rust sell the components separately: cheaper and more flexible when you want three of the four, more work when you genuinely wanted the package.
saying these in an interview costs you the question
- Claiming Go and Rust simply cannot share behaviour, ignoring trait default methods and embedding
- Saying Rust traits are just interfaces -- they carry default bodies that re-dispatch through self
- Treating a subclass-hook framework design as portable rather than requiring redesign
- Not recognising that shared state, not shared behaviour, is where the omission actually costs
- Presenting the omission as pure upside and skipping forwarding boilerplate, the orphan rule and the Deref anti-pattern