Go’s lack of inheritance is deliberate. The Go designers observed that inheritance hierarchies tend to create tight coupling and fragile base classes — problems that composition avoids. Embedding is Go’s tool for composition: you can embed one type in another and promote its methods, without inheritance’s downsides.
It’s more powerful and more subtle than it appears.
What Embedding Does
| |
Logger’s Log method is promoted to Service. You can call it directly on Service values. The embedded Logger is accessible as svc.Logger for explicit access.
This is not inheritance. Service is not a Logger. There’s no polymorphism — you can’t pass a *Service where a *Logger is expected. Embedding is a syntactic convenience for accessing the embedded type’s fields and methods.
Interface Satisfaction Through Embedding
Where embedding becomes powerful is interface satisfaction:
| |
By embedding io.Writer, BufferedWriter automatically satisfies the Writer interface. You can then override specific methods while keeping the rest:
| |
Now BufferedWriter buffers writes and flushes when full, delegating to the embedded io.Writer for actual I/O.
The Composition Pattern for Services
A common pattern at the European fintech firm: embedding common infrastructure into service types.
| |
Both services have access to logging, metrics, and tracing through the promoted methods. Updates to BaseService (adding a new infrastructure field, for example) automatically propagate to all embedded types.
Embedding Interfaces (Not Just Structs)
You can embed interfaces in structs to create partial implementations:
| |
The TTLCache wraps any ReadOnlyCache and adds expiry. It overrides Get to check expiry, and inherits Keys() by delegation to the embedded interface’s value.
The important caveat: if you use this pattern and the embedded interface field is nil, any call to a promoted method that isn’t overridden will panic. Always initialise embedded interfaces:
| |
What Embedding Is Not
Not polymorphism: you can’t use a *Service value where a *Logger is expected, even though Service embeds Logger. Embedding promotes methods; it doesn’t establish a subtype relationship.
Not method override with super: calling svc.Log("x") always calls Logger.Log. There’s no mechanism like Java’s super.Log() to call the embedded type’s method from an overriding method — you call it explicitly via svc.Logger.Log("x").
Not always the right tool: if you need polymorphism (pass different concrete types where an interface is expected), use interface composition. Embedding is for method promotion and delegation.
The Rule
Embed when:
- You want to promote methods to the outer type for ergonomic access
- You’re building a decorator/wrapper that overrides some methods and delegates others
- Multiple types share common infrastructure and you want a single definition
Don’t embed when:
- You need subtype polymorphism (use interfaces)
- The embedded type is conceptually a field, not a capability the outer type has (named fields are clearer)
- The embedded type has methods you don’t want to expose (they’ll all be promoted)
Embedding is one of Go’s more elegant design choices — a composition mechanism that’s simple, explicit, and doesn’t require the type system complexity that inheritance brings. Using it correctly produces code that’s easy to read and extend.