Embedding and Composition Intermediate¶
Introduction¶
Go has no inheritance. Instead, it uses composition via embedding -- a struct or interface can include another type directly, gaining its fields and methods without subclassing. Embedded fields and methods are promoted to the outer type, giving the appearance of inheritance while maintaining the simplicity and flexibility of composition. This is not syntactic sugar for inheritance -- there's no "is-a" relationship, no virtual dispatch, and no fragile base class problem. Understanding embedding, method promotion, shadowing, and the pattern of embedding interfaces in structs (for testing and partial implementation) is essential for Go interviews and idiomatic code.
Struct Embedding¶
Embedding a type in a struct promotes all its fields and methods to the outer struct.
type Address struct {
Street string
City string
State string
Zip string
}
type Employee struct {
Name string
Address // embedded -- no field name, just the type
Salary float64
}
func main() {
emp := Employee{
Name: "Alice",
Address: Address{
Street: "123 Main St",
City: "Portland",
State: "OR",
Zip: "97201",
},
Salary: 95000,
}
// Promoted fields -- access directly
fmt.Println(emp.City) // "Portland" -- promoted from Address
fmt.Println(emp.State) // "OR"
// Still accessible through the embedded type name
fmt.Println(emp.Address.City) // "Portland" -- explicit access
}
Method Promotion¶
Methods on the embedded type are promoted to the outer type.
type Logger struct {
Prefix string
}
func (l *Logger) Log(msg string) {
fmt.Printf("[%s] %s\n", l.Prefix, msg)
}
func (l *Logger) Logf(format string, args ...any) {
fmt.Printf("[%s] %s\n", l.Prefix, fmt.Sprintf(format, args...))
}
type Server struct {
*Logger // embed pointer to Logger
Addr string
}
func main() {
srv := &Server{
Logger: &Logger{Prefix: "HTTP"},
Addr: ":8080",
}
// Logger methods are promoted to Server
srv.Log("starting") // "[HTTP] starting"
srv.Logf("listening on %s", srv.Addr) // "[HTTP] listening on :8080"
}
Embedding Pointers vs Values
You can embed either T or *T. Embedding a pointer (*Logger) means multiple structs can share the same embedded instance, and the embedded field can be nil (requiring nil checks).
Interface Satisfaction Through Embedding¶
When a struct embeds a type, the embedded type's methods are promoted, which means the outer struct can satisfy interfaces that the embedded type satisfies.
type Reader interface {
Read(p []byte) (n int, err error)
}
type CountingReader struct {
io.Reader // embeds the interface -- any io.Reader will do
BytesRead int64
}
func (cr *CountingReader) Read(p []byte) (int, error) {
n, err := cr.Reader.Read(p) // delegate to the embedded Reader
cr.BytesRead += int64(n)
return n, err
}
// CountingReader satisfies io.Reader
func process(r io.Reader) { /* ... */ }
func main() {
file, _ := os.Open("data.txt")
cr := &CountingReader{Reader: file}
process(cr) // works -- CountingReader satisfies io.Reader
fmt.Printf("Read %d bytes\n", cr.BytesRead)
}
Interface Embedding¶
Interfaces embed other interfaces to compose larger contracts.
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// Composed from smaller interfaces
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
// Equivalent to writing all methods explicitly:
type ReadWriteCloser interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
Close() error
}
Standard Library Examples
io.ReadWriter, io.ReadCloser, io.ReadWriteCloser, io.ReadWriteSeeker are all composed through interface embedding.
Shadowing / Overriding Embedded Methods¶
The outer type can define a method with the same name as a promoted method. The outer method shadows the embedded one.
type Base struct{}
func (b Base) Greet() string {
return "Hello from Base"
}
type Derived struct {
Base
}
// Shadows (overrides) the embedded Base.Greet
func (d Derived) Greet() string {
return "Hello from Derived"
}
func main() {
d := Derived{}
fmt.Println(d.Greet()) // "Hello from Derived" -- shadowed
fmt.Println(d.Base.Greet()) // "Hello from Base" -- explicit access
}
This Is Not Polymorphism
Unlike virtual method dispatch in OOP, embedding doesn't support polymorphic calls. If Base calls Greet() internally, it always calls Base.Greet(), never the outer type's version:
Embedding Interfaces in Structs (Testing Pattern)¶
Embedding an interface in a struct is a powerful pattern for partial implementation and test mocks.
// Production interface
type UserStore interface {
Get(id int) (*User, error)
Create(u *User) error
Update(u *User) error
Delete(id int) error
List() ([]*User, error)
}
// Mock that only implements the methods you need in a test
type mockUserStore struct {
UserStore // embed the interface -- all methods "exist" (panic if called)
getFn func(id int) (*User, error)
createFn func(u *User) error
}
func (m *mockUserStore) Get(id int) (*User, error) {
return m.getFn(id)
}
func (m *mockUserStore) Create(u *User) error {
return m.createFn(u)
}
// In tests: only define the methods the test actually calls
func TestGetUser(t *testing.T) {
store := &mockUserStore{
getFn: func(id int) (*User, error) {
return &User{ID: id, Name: "Alice"}, nil
},
}
// Use store -- if test accidentally calls Update/Delete/List, it panics
// with a clear "nil pointer dereference" pointing to the unimplemented method
}
Why This Works
Embedding the interface means the struct nominally satisfies the interface (all methods are promoted). The embedded interface value is nil, so calling any un-overridden method panics -- which is exactly what you want in tests: fail loudly on unexpected calls.
Multiple Embedding and Ambiguity¶
When two embedded types promote a field or method with the same name, direct access is ambiguous and causes a compile error.
type A struct{}
func (A) Hello() string { return "A" }
type B struct{}
func (B) Hello() string { return "B" }
type C struct {
A
B
}
func main() {
c := C{}
// c.Hello() // COMPILE ERROR: ambiguous selector c.Hello
c.A.Hello() // OK: explicit access
c.B.Hello() // OK: explicit access
}
The outer type can resolve ambiguity by defining its own method:
Real-World Embedding Examples¶
HTTP Server with Embedded Logger¶
type App struct {
*http.Server
*log.Logger
config Config
}
func NewApp(cfg Config) *App {
app := &App{
Server: &http.Server{
Addr: cfg.Addr,
ReadTimeout: cfg.ReadTimeout,
WriteTimeout: cfg.WriteTimeout,
},
Logger: log.New(os.Stdout, "[APP] ", log.LstdFlags),
config: cfg,
}
return app
}
func (a *App) Start() error {
a.Println("Starting server on", a.Addr) // Logger.Println promoted
return a.ListenAndServe() // http.Server.ListenAndServe promoted
}
Wrapping sync.Mutex¶
type SafeMap struct {
sync.RWMutex // embed the mutex -- Lock/Unlock promoted
data map[string]string
}
func NewSafeMap() *SafeMap {
return &SafeMap{data: make(map[string]string)}
}
func (m *SafeMap) Get(key string) (string, bool) {
m.RLock()
defer m.RUnlock()
v, ok := m.data[key]
return v, ok
}
func (m *SafeMap) Set(key, value string) {
m.Lock()
defer m.Unlock()
m.data[key] = value
}
Exported Mutex Methods
Embedding sync.Mutex promotes Lock and Unlock as exported methods of your type. If your type is exported, this leaks concurrency details to callers. Prefer an unexported named field (mu sync.Mutex) for exported types.
Embedding vs Named Fields¶
| Aspect | Embedding | Named Field |
|---|---|---|
| Syntax | type S struct { T } |
type S struct { t T } |
| Promotion | Fields and methods promoted | No promotion; access via s.t.Method() |
| Interface satisfaction | Outer type gains embedded type's methods | Must explicitly delegate |
| Naming conflicts | Can cause ambiguity | No conflicts -- explicit field name |
| Encapsulation | Exposes all promoted methods | Better control over API surface |
Quick Reference¶
| Concept | Syntax | Notes |
|---|---|---|
| Struct embedding | type S struct { T } |
Promotes T's fields and methods |
| Pointer embedding | type S struct { *T } |
Promotes T's methods; shared instance; can be nil |
| Interface embedding | type I interface { J; K } |
Combines method sets |
| Interface in struct | type S struct { I } |
Satisfies I; panics on unimplemented methods |
| Shadowing | Define method with same name on outer type | Outer method takes priority |
| Ambiguity | Two embedded types with same method | Compile error; resolve explicitly |
| Promoted access | outer.Field |
Same as outer.Embedded.Field |
Best Practices¶
- Favor composition over inheritance -- embed types to reuse behavior, not to model "is-a" relationships
- Keep embedding shallow -- deeply nested embeddings create confusing method promotion chains
- Prefer named fields for unexported types -- embedding exposes the inner type's API, which may be more than you want
- Use interface embedding in structs for test mocks -- a powerful, low-boilerplate way to partially implement interfaces
- Don't embed
sync.Mutexin exported types -- use a named unexported field to avoid leaking Lock/Unlock - Resolve ambiguity explicitly -- if two embedded types conflict, define the method on the outer type
- Document the embedding relationship if it affects the type's public API
Common Pitfalls¶
Nil Embedded Pointer
When embedding a pointer type, always initialize it before accessing promoted fields.Promoted Methods Aren't Polymorphic
type Base struct{}
func (b Base) Name() string { return "Base" }
func (b Base) Greet() string { return "I am " + b.Name() }
type Child struct{ Base }
func (c Child) Name() string { return "Child" }
c := Child{}
fmt.Println(c.Greet()) // "I am Base" -- NOT "I am Child"
Base.Greet always calls Base.Name. Go has no virtual method dispatch.
Embedding Exports More Than You Expect
Embedding a type promotes all its exported methods. If you embed bytes.Buffer, your type suddenly has Read, Write, String, Len, Reset, Grow, and many more methods. Be intentional about what you expose.
Ambiguous Selectors
type A struct{}; func (A) Close() error { return nil }
type B struct{}; func (B) Close() error { return nil }
type C struct { A; B }
c := C{}
c.Close() // COMPILE ERROR: ambiguous selector c.Close
Close on C to resolve, or use named fields instead of embedding.
Performance Considerations¶
| Scenario | Impact |
|---|---|
| Promoted method call | Zero overhead -- compiler resolves promotion at compile time |
| Value embedding | Embedded value is stored inline; no extra allocation |
| Pointer embedding | 8-byte pointer; shared instance avoids copying large embedded types |
| Interface embedding in struct | Interface header (16 bytes); method calls go through indirect dispatch |
| Deep embedding chains | No runtime cost -- all resolved at compile time |
Memory Layout
Embedded fields are laid out inline in the struct's memory. A struct { A; B } where A is 24 bytes and B is 16 bytes occupies 40 bytes (plus padding). There's no pointer indirection for value embedding.
Interview Tips¶
Interview Tip
When asked "Does Go have inheritance?", answer: No. Go uses composition through embedding. Struct embedding promotes fields and methods, giving a similar effect, but there is no "is-a" relationship and no virtual dispatch. The embedded type doesn't know it's embedded.
Interview Tip
Explain the key difference from inheritance: in OOP, a base class method calling a virtual method dispatches to the subclass. In Go, an embedded type's method always calls its own methods, never the outer type's shadowing methods. This avoids the fragile base class problem.
Interview Tip
The "embed interface in struct for testing" pattern is a great answer when asked about mocking in Go. Explain: embedding the interface satisfies it nominally, so you only need to override the methods your test actually exercises. Unexpected calls panic immediately, catching test bugs.
Interview Tip
If asked about sync.Mutex embedding, explain the trade-off: embedding promotes Lock() and Unlock() to the outer type. This is convenient for unexported types but leaks implementation details for exported types. Prefer mu sync.Mutex as a named field in exported structs.
Key Takeaways¶
- Go uses composition via embedding, not inheritance
- Embedding promotes fields and methods to the outer type
- Promoted methods allow the outer type to satisfy interfaces of the embedded type
- Shadowing lets the outer type override promoted methods, but there's no virtual dispatch
- Embedding interfaces in structs is a powerful testing pattern for partial mocks
- Pointer embedding enables sharing and nil states; value embedding is inline
- Multiple embedding can cause ambiguity -- resolve by defining the method on the outer type
- Don't embed
sync.Mutexin exported types -- prefer a named unexported field - All promotion is resolved at compile time with zero runtime overhead