Go Favor Composition: Struct Embedding
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, ifEmployeeinherits fromPerson, you can treat anEmployeeobject as aPerson. For example, a function expecting aPersonwould happily accept anEmployee. This creates a tightly coupled, hierarchical relationship.Go Struct Embedding (
has-a): Go creates ahas-arelationship. AnEmployeehas aPersoninside it, but anEmployeeis not aPerson. You cannot pass anEmployeeto a function that expects aPersonwithout explicitly passing the embedded field (myEmployee.Person).
| Is-a (Inheritance) | Has-a (Struct Embedding) | |
| Relationship | Hierarchical parent-child | Composed, independent objects |
| Interchangeable? | Yes, child can be treated as parent | No, they are distinct types |
| Dependency | Tight, rigid coupling | Loose coupling through composition |
| Method Access | Inherited from parent | Promoted 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 strictis-arelationship, 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/