Skip to content

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:

func (b Base) Introduce() string {
    return b.Greet() + " -- nice to meet you"
}

d := Derived{}
fmt.Println(d.Introduce()) // "Hello from Base -- nice to meet you"
// NOT "Hello from Derived" -- Base.Introduce calls Base.Greet, not Derived.Greet


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:

func (c C) Hello() string {
    return c.A.Hello() + " and " + c.B.Hello()
}

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

  1. Favor composition over inheritance -- embed types to reuse behavior, not to model "is-a" relationships
  2. Keep embedding shallow -- deeply nested embeddings create confusing method promotion chains
  3. Prefer named fields for unexported types -- embedding exposes the inner type's API, which may be more than you want
  4. Use interface embedding in structs for test mocks -- a powerful, low-boilerplate way to partially implement interfaces
  5. Don't embed sync.Mutex in exported types -- use a named unexported field to avoid leaking Lock/Unlock
  6. Resolve ambiguity explicitly -- if two embedded types conflict, define the method on the outer type
  7. Document the embedding relationship if it affects the type's public API

Common Pitfalls

Nil Embedded Pointer

type Outer struct {
    *Inner
}
type Inner struct{ Name string }

o := Outer{} // Inner is nil
fmt.Println(o.Name) // PANIC: nil pointer dereference
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"
The embedded 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
Define 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.Mutex in exported types -- prefer a named unexported field
  • All promotion is resolved at compile time with zero runtime overhead