Fundamentals 10 min read

Why Go and Rust Reject Inheritance: Embracing Composition Over Class Hierarchies

Developers transitioning from Java or C# to Go or Rust often miss class inheritance, but both languages deliberately avoid it; this article explains the pitfalls of inheritance, the benefits of composition, interfaces, and traits, and offers practical guidance for designing maintainable, concurrent systems without inheritance.

Code Wrench
Code Wrench
Code Wrench
Why Go and Rust Reject Inheritance: Embracing Composition Over Class Hierarchies

1. The "Original Sin" of Inheritance: Fragile Base Classes

In traditional object‑oriented programming, inheritance is one of the three pillars, but in large codebases it often becomes a maintenance nightmare. The classic "Fragile Base Class Problem" occurs when a base class changes—adding a private method or a new field—causing subclasses to break or behave incorrectly. Inheritance therefore creates strong coupling: subclasses inherit not only functionality but also implementation details and change‑risk.

Joe Armstrong, the creator of Erlang, famously said: “You wanted a banana, but you got a gorilla holding a banana and the whole jungle.”

In Go and Rust, maintainability is prioritized over raw code reuse. If reuse introduces uncontrollable coupling, it is rejected.

2. Go's Solution: Composition and Implicit Interfaces

Go has no class keyword; it only has struct and extends does not exist. Instead, Go uses embedding to achieve composition.

Struct Embedding ≠ Inheritance

Many beginners mistake Go's embedding for inheritance. The following example shows how embedding works:

package main

import "fmt"

type Base struct {
    Name string
}

func (b *Base) Hello() {
    fmt.Println("Hello from Base")
}

type Derived struct {
    Base // embedding
}

func main() {
    d := Derived{}
    d.Hello() // ✅ method is promoted
    // var b *Base = &d // ❌ compile error: cannot assign *Derived to *Base
}

Although Derived can call Hello(), it is not a polymorphic subtype of Base. You cannot pass a *Derived where a *Base is required.

Go’s intent is clear: composition enables code reuse without establishing a type hierarchy. This is a "has‑a" relationship, not an "is‑a" relationship.

Interfaces: Behavioural Polymorphism

Go’s polymorphism relies on interfaces, not class trees.

Implicit implementation : No implements keyword. Any type that defines the required methods satisfies the interface automatically.

Decoupling : Interfaces are defined by the caller and implemented by the provider.

Small‑interface philosophy : Go encourages interfaces with a single method, such as io.Reader, rather than large abstract classes.

This design makes systems extremely flexible: any type, even built‑in types, can be given new behaviour without altering an inheritance tree.

3. Rust's Solution: Traits and Memory Layout

Rust goes further by caring about both abstraction and memory. A Trait in Rust is similar to Go’s interface combined with Java’s default methods.

trait Animal {
    fn speak(&self);
}

struct Dog;

impl Animal for Dog {
    fn speak(&self) { println!("Woof"); }
}

In Rust, data types (structs) and behaviour (traits) are orthogonal. You can implement new traits for a struct without modifying the struct definition, breaking the rigid "define‑once‑lock‑in" model of inheritance.

Zero‑Cost Abstractions and Controllable Memory

Both Go and Rust value predictable memory layout.

Rust’s traits are monomorphized at compile time, eliminating runtime v‑table overhead unless you use dyn Trait.

Composition (structs containing other structs) yields compact, controllable layouts, especially when combined with #[repr(C)], which is crucial for high‑performance and cache‑friendly code.

4. Deep Dive: Concurrency Safety

Inheritance combined with mutable state creates hidden concurrency hazards. In OOP, a parent class often holds mutable state that subclasses modify, leading to data races in multithreaded environments.

Go’s CSP model : Go encourages "communicating sequential processes" and designs structs to be stateless or to encapsulate state inside goroutines, avoiding shared mutable state.

Rust’s ownership system : Ownership makes it explicit who owns which piece of memory. Inheritance would blur ownership, so Rust relies on composition and explicit borrowing checks to enforce safe concurrency.

Thus, inheritance tends to encourage deep shared mutable state, a major risk in modern concurrent systems.

5. Mind‑Shift: Practical Advice for Developers

Abandoning inheritance requires a change in thinking. When you feel the urge to use extends in Go or Rust, follow these three steps:

Question the relationship : Is the object truly an "is‑a" relationship, or does it merely "have" the capabilities of another object?

Extract behaviour : Can the common logic be factored into a function, a trait, or an interface?

Use composition : Combine structs (e.g., struct { User }) and expose only the needed methods via the delegate pattern.

Explicit composition is preferable to implicit inheritance. In an inheritance hierarchy, finding a method implementation may require navigating many files, whereas composition makes dependencies obvious and refactoring safe.

6. Conclusion

Go and Rust give up inheritance not to be trendy but to reduce long‑term maintenance costs in large‑scale, high‑concurrency, and system‑level projects. They demonstrate that elegant, efficient, and powerful software can be built without class hierarchies, by favoring data composition and orthogonal behaviour.

Further Reading

Go Official Documentation: Effective Go

Rust Book: Traits

Design Patterns: Composition Over Inheritance

software designTraits
Code Wrench
Written by

Code Wrench

Focuses on code debugging, performance optimization, and real-world engineering, sharing efficient development tips and pitfall guides. We break down technical challenges in a down-to-earth style, helping you craft handy tools so every line of code becomes a problem‑solving weapon. 🔧💻

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.