Skip to main content

Command Palette

Search for a command to run...

Go Favor Composition: Struct Embedding

Updated
•4 min read•View as Markdown

Let’s explore another Go unique design: Embedding. This post is for first type of embedding: struct embedding, where one struct type embeds another struct type.

Coming from an object-oriented programming (OOP) language like Java or C++, the way Go handles inheritance might seem a bit... empty. You won't find extends keywords or complex class hierarchies. Instead, Go offers a more elegant and flexible alternative: struct embedding.

At first glance, embedding looks like inheritance with "method promotion"—the outer struct seems to inherit the fields and methods of the inner struct. But this is a key difference in philosophy, and understanding it is crucial for writing idiomatic Go.

What is struct embedding?

In Go, struct embedding is a powerful form of composition. You embed a type, like a struct, into another struct simply by declaring it without a field name. So we don’t need to write “delegation” method simply forwarding calls to underlying struct type.

Consider a Person struct that we want to reuse in an Employee struct.

type Person struct {
    Name  string
    Email string
}

func (p Person) Greet() {
    fmt.Printf("Hello, my name is %s.\n", p.Name)
}

type Employee struct {
    Person // Embedded struct
    ID     int
}

e := Employee{
    Person: Person{Name: "Alice", Email: "alice@example.com"},
    ID:     123,
}

fmt.Println(e.Name)  // Prints "Alice"
e.Greet() // // Prints "Hello, my name is Alice."

The key difference: has-a vs. is-a

This brings us to the most important distinction between Go's embedding and classic OOP inheritance:

  • Classic OOP Inheritance (is-a): In a language like Java, if Employee inherits from Person, you can treat an Employee object as a Person. For example, a function expecting a Person would happily accept an Employee. This creates a tightly coupled, hierarchical relationship.

  • Go Struct Embedding (has-a): Go creates a has-a relationship. An Employee has a Person inside it, but an Employee is not a Person. You cannot pass an Employee to a function that expects a Person without explicitly passing the embedded field (myEmployee.Person).

Is-a (Inheritance)Has-a (Struct Embedding)
RelationshipHierarchical parent-childComposed, independent objects
Interchangeable?Yes, child can be treated as parentNo, they are distinct types
DependencyTight, rigid couplingLoose coupling through composition
Method AccessInherited from parentPromoted from embedded field

This explicit separation makes the code more modular, testable, and easier to reason about. The compiler does the hard work of forwarding method calls for you, but the underlying types remain distinct (loosely coupled).

Why Go was designed this way

The main reason I think is for testability.

  • The "Fragile Base Class" Problem:
    In inheritance, a change to a base class can unintentionally break subclasses in unexpected ways. Go's composition-based approach prevents these cascading, invisible changes.

  • Complex Hierarchies:
    Deep inheritance trees are difficult to manage, understand, and debug. Go's approach promotes flatter, more maintainable code with clear dependencies.

  • Forced Relationship:
    Inheritance forces a strict is-a relationship, even when a more flexible relationship is needed. Composition allows you to mix and match behaviors as required. As the famous quote goes, "You wanted a banana but what you got were a gorilla holding the banana and the entire jungle".

Tradeoffs

  • Less intuitive for newcomers:
    This is always true when we switch between classical OOP inheritance, the concept of embedding can take some getting used to.

  • Shadowing and complexity:
    If an outer struct and an embedded struct have fields or methods with the same name, the outer one takes precedence. This can create confusion if not handled carefully.

  • Manual delegation (sometimes):
    The method promotion from embedding is "shallow." If you need to manipulate the embedded struct in a complex way, you might find yourself writing manual delegation code, though this is often a sign that you should structure your code differently.

But more importantly, it can lead to a potential “abstraction leak”. See example below.

Real Use Cases

In the lruSessionCache, it embeds sync.Mutex . This allows any method on lruSessionCache to directly call Lock() and Unlock() on itself, as if they were its own methods.

// From: crypto/tls/common.go
type lruSessionCache struct {
    sync.Mutex
    m        map[string]*list.Element
    q        *list.List
    capacity int
}

func (c *lruSessionCache) Get(key string) (session *SessionState, ok bool) {
    c.Lock() // Accesses the embedded mutex's Lock() method directly
    defer c.Unlock() // Accesses the embedded mutex's Unlock() method

    if elem, ok := c.m[key]; ok {
        // ... logic for cache hit ...
    }

    return nil, false
}

Looks neat, right? But by embedding a public type like sync.Mutex, you promote its public methods (Lock and Unlock) to the outer struct's API, potentially exposing an implementation detail that was meant to be private. Otherwise

Here lruSessionCache is private type inside crypto, it’s kind of safe to use. Otherwise if we can import LRUSessionCache from outside, there will risk of misuse if external directly access Mutex public methods via cache struct.

cache.Lock() // unexpected use

As with all design choices, there are trade-offs to consider. By understanding these trade-offs, we can make better informed decisions.

This tweet offers another approach to deal with safe “Mutex” situation.


https://eli.thegreenplace.net/2020/embedding-in-go-part-3-interfaces-in-structs/