Struct embedding

Struct embedding in Go promotes fields and methods from an anonymous inner struct to the outer struct for code reuse and composition.

The composition shortcut

You are building a service that tracks inventory. You need a Product type that stores a name, price, and SKU. Later you need a PromotedProduct type that has everything a regular product has, plus a discount percentage and an expiration date. In languages with class inheritance, you create a base class and extend it. Go does not have classes. It does not have inheritance. Instead, it gives you a different tool that feels similar but operates on entirely different rules. You put one struct inside another without giving it a name. The compiler automatically lifts the inner fields and methods to the outer level. This is struct embedding.

How embedding actually works

Think of embedding like a tool belt. A carpenter carries a hammer and a tape measure. You do not need to reach into a drawer to use them. They are already attached to your belt. When someone asks the carpenter to measure a board, they just hand over the tape measure. The belt does not change what the carpenter is. It just makes the tools immediately available.

Go uses this pattern for composition. You are not creating a parent-child relationship. You are attaching a module to a larger type. The outer struct gains access to the inner struct's fields and methods, but the inner struct remains its own independent type. This distinction matters when you start passing values around or implementing interfaces. The language treats the embedded field as a regular field that happens to share the type's name. You can still access it directly if you write out the full path. The shorthand is just a compiler convenience.

Go naming conventions keep this pattern readable. Public names start with a capital letter. Private names start lowercase. There are no public or private keywords. The receiver name is usually one or two letters matching the type, like (p *Product), not (this *Product) or (self *Product). Stick to the convention and the code stays predictable.

Embedding is not inheritance. The outer type does not become a subtype of the inner type. You cannot pass a PromotedProduct to a function that expects a Product unless both satisfy the same interface. The language forces you to be explicit about type boundaries. Composition over inheritance is the guiding principle here. Attach behavior. Do not fake a hierarchy.

A minimal example

Here is the simplest form of embedding. We define a base struct, embed it inside another, and call a promoted method.

package main

import "fmt"

// Logger prints messages with a prefix.
type Logger struct {
    Prefix string
}

// Log formats and prints a message.
func (l *Logger) Log(msg string) {
    fmt.Printf("[%s] %s\n", l.Prefix, msg)
}

// Server attaches a Logger to itself.
type Server struct {
    Logger // Embedded anonymously to promote fields and methods
    Port   int
}

func main() {
    s := Server{
        Logger: Logger{Prefix: "HTTP"}, // Initialize the embedded field explicitly
        Port:   8080,
    }
    s.Log("starting up") // Promoted method call, equivalent to s.Logger.Log
}

What happens under the hood

The compiler sees s.Log("starting up") and checks the Server struct. It does not find a Log method directly on Server. It then looks at the embedded Logger field. It finds the method and promotes it. The call resolves to s.Logger.Log("starting up"). You can write the longer version if you want, but Go lets you skip the intermediate step.

Field promotion works the same way. You can access s.Prefix directly, even though Prefix belongs to Logger. The compiler treats it as a shorthand for s.Logger.Prefix. This shorthand only works one level deep. If you embed a struct inside another embedded struct, you must write out the full path. Go does not chain promotion automatically.

The receiver type matters. The Log method uses a pointer receiver *Logger. When you call s.Log(), Go automatically takes the address of the embedded field. You do not need to write &s.Logger. The language handles the indirection behind the scenes. Pointer receivers work on both struct values and struct pointers. Value receivers only work on values. Keep your receiver types consistent across a package to avoid confusing method set mismatches.

Memory layout stays flat. The embedded struct's fields are laid out contiguously inside the outer struct. There is no extra pointer indirection just because you embedded. The compiler treats the embedded type as a group of fields that happen to belong to a named type. This keeps allocation cheap and cache locality high.

Method sets follow strict rules. A struct type S has a method set containing only methods with receiver S. A pointer type *S has a method set containing methods with receiver S and *S. When you embed Logger inside Server, the promoted methods inherit the receiver rules of the embedded type. If Logger implements an interface, Server automatically satisfies it too. This automatic satisfaction is where embedding shines. You get interface compliance without writing forwarding methods.

Real-world usage

Real code rarely stops at one level. You usually embed to satisfy an interface or to share configuration across multiple handlers. Here is a pattern that shows up in production services: a base handler that manages request context and logging, embedded into specific route handlers.

package main

import (
    "context"
    "fmt"
    "net/http"
)

// BaseHandler provides shared request plumbing.
type BaseHandler struct {
    ServiceName string
}

// WithContext attaches the base handler to a standard http.Handler.
func (b *BaseHandler) WithContext(next http.Handler) http.Handler {
    // Return a wrapper that injects service metadata into the request context
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := context.WithValue(r.Context(), "service", b.ServiceName)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// UserHandler handles user routes and reuses BaseHandler.
type UserHandler struct {
    BaseHandler // Embed to gain access to WithContext and ServiceName
    DB          string
}

// ServeHTTP satisfies the http.Handler interface.
func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    fmt.Fprintf(w, "Service: %v, DB: %s\n", ctx.Value("service"), h.DB)
}

func main() {
    h := &UserHandler{
        BaseHandler: BaseHandler{ServiceName: "auth"},
        DB:          "postgres",
    }
    // Promoted method wraps the handler.
    wrapped := h.WithContext(h)
    wrapped.ServeHTTP(nil, &http.Request{})
}

The UserHandler gets WithContext for free. It also satisfies http.Handler because it defines ServeHTTP. The embedded BaseHandler does not implement http.Handler, so there is no conflict. If both types implemented the same interface, the compiler would reject the program with ambiguous selector UserHandler.ServeHTTP. You would need to explicitly define which implementation wins.

This pattern scales well. You can embed a MetricsCollector, a Tracer, or a Cache into any handler that needs it. Each module stays isolated. You can swap implementations without touching the outer struct. The only coupling is the struct definition itself.

Where it breaks

Embedding looks like inheritance, but it breaks the moment you try to override a method or change the type hierarchy. If you define a Log method directly on Server, the compiler rejects the program with method redeclared: Server.Log. Go does not allow method overriding. The embedded method and the outer method would collide, and the language refuses to guess which one you want.

Field shadowing causes a different problem. If Server defines its own Prefix field, the compiler complains with duplicate field name Prefix in struct literal. You cannot have two fields with the same name at the same level, even if one comes from an embedded type. The fix is to rename the outer field or remove the embedding.

Interface satisfaction is another trap. If Logger implements an interface, Server automatically satisfies it too. This is usually helpful, but it becomes dangerous when you embed multiple types that implement the same interface. The compiler panics at compile time with ambiguous selector Server.InterfaceMethod. You must explicitly define which implementation wins.

Another common mistake is treating the embedded field as a value when the methods expect a pointer. If Log used a value receiver func (l Logger) Log(...), calling s.Log() on a struct value works fine. But if you pass s to a function that expects a pointer to Server, the embedded pointer methods still work because Go takes the address automatically. The rule is simple: pointer receivers work on both values and pointers. Value receivers only work on values. Keep your receiver types consistent across a package.

Zero values behave predictably but can surprise you. An uninitialized embedded struct contains the zero value of its type. If you embed a pointer type like *Logger, the zero value is nil. Calling a promoted method on a nil embedded pointer triggers a runtime panic. Always initialize embedded pointers or use value types when nil is not a valid state.

When to embed and when to reach for something else

Use struct embedding when you want to attach a self-contained module to a larger type without writing boilerplate forwarding methods. Use a regular named field when you need to override behavior, change the type at runtime, or keep the inner type strictly isolated. Use an interface when you want to define a contract that multiple unrelated types can satisfy. Use composition with explicit delegation when you need to intercept calls, add logging, or modify arguments before they reach the inner type. Use plain sequential code when you do not need composition: the simplest thing that works is usually the right thing.

Where to go next