What Is Struct Embedding in Go (Composition Over Inheritance)

Struct embedding in Go enables composition by including one struct inside another, promoting its fields and methods for reuse without inheritance.

The problem with base classes

You are building a service that handles user requests. Every request needs to log a timestamp and a trace ID. You write a Logger struct with a Log method. Now you have five different services, and you realize you are copy-pasting the logger fields and methods into each one. Or worse, you are tempted to create a base class that all services inherit from.

Go does not have base classes. Go does not have inheritance. Go has struct embedding. Embedding lets you put a struct inside another struct and get its methods for free. You reuse behavior without the complexity of inheritance hierarchies. You get a "has-a" relationship with the convenience of an "is-a" call.

Composition with promotion

Struct embedding is composition. When you embed a struct, the outer struct contains the inner struct as a field. The compiler promotes the inner struct's fields and methods to the outer struct's level. This means you can call outer.Method() and it actually calls outer.Inner.Method(). It is syntactic sugar for delegation. You get the behavior of the inner type without writing wrapper methods.

Think of a smartphone. It has a battery. You do not say the phone "is" a battery. You say it has a battery. But when you press the power button, the phone delegates to the battery to check the charge. Embedding is like wiring the battery's status light directly to the phone's screen. You check the phone, and it shows the battery level because the phone embeds the battery's interface. The phone has the battery, but you interact with the phone to get the battery's data.

Embedding promotes both methods and fields. If the inner struct has a field prefix, the outer struct can access outer.prefix directly. Visibility rules still apply. Unexported fields are not accessible from outside the package, even if promoted. The promotion happens at the package level.

Embedding is delegation with less typing.

Minimal example

Here is the simplest embedding: a service embeds a logger and calls the logger's method directly.

package main

import "fmt"

// Logger holds configuration for log messages.
type Logger struct {
    // prefix adds context to every log line.
    prefix string
}

// Log prints a message with the logger's prefix.
func (l *Logger) Log(msg string) {
    // Concatenate prefix and message for output.
    fmt.Println(l.prefix + ": " + msg)
}

// Service represents a business unit with its own name.
type Service struct {
    // Embedding Logger promotes Log to Service.
    Logger
    name string
}

func main() {
    // Create a Service with an embedded Logger.
    s := Service{
        Logger: Logger{prefix: "INFO"},
        name:   "API",
    }

    // Call Log directly on Service.
    // The compiler rewrites this to s.Logger.Log.
    s.Log("Started")
}

The code defines a Logger with a Log method. Service embeds Logger. In main, we create a Service and call s.Log("Started"). The output is INFO: Started. The Service did not define Log. The compiler found Log on the embedded Logger and promoted it.

How the compiler rewrites your code

When you write s.Log("Started"), the Go compiler checks Service. It does not find a Log method defined on Service. It looks at the embedded fields. It finds Logger. It sees Logger has a Log method. The compiler rewrites the call to s.Logger.Log("Started"). This happens at compile time. There is no dynamic dispatch overhead for the promotion itself.

The method receiver is still *Logger. The receiver value passed to Log is the address of the embedded Logger field inside Service. If the embedded field is a value, the compiler takes its address automatically, provided the outer struct is addressable. If s is a variable, &s.Logger is valid. If you try to call a promoted method on a non-addressable value, the compiler rejects the program with cannot take the address of s.Logger.

Embedding also affects interface satisfaction. If Logger implements an interface, Service automatically implements that interface too. The method set of Service includes the promoted methods. This allows you to embed a type to satisfy an interface without writing any new code.

Shadowing works explicitly. If you define a Log method on Service, it shadows the embedded Log. The outer method wins. You can override behavior by defining a method with the same name on the outer struct. To call the inner method from the outer method, you must use the field name: s.Logger.Log().

Realistic usage: Handlers and dependencies

Here is a realistic handler embedding a logger to satisfy http.Handler while keeping logging reusable.

package main

import (
    "fmt"
    "net/http"
)

// Logger provides structured logging.
type Logger struct {
    level string
}

// Info logs a message with the configured level.
func (l *Logger) Info(msg string) {
    fmt.Printf("[%s] %s\n", l.level, msg)
}

// Handler embeds Logger to gain logging methods.
type Handler struct {
    Logger
    name string
}

// ServeHTTP handles the request using promoted methods.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // Call Info directly; compiler promotes to h.Logger.Info.
    h.Info("Handling request")
    fmt.Fprintln(w, "OK")
}

func main() {
    // Construct handler with embedded logger.
    h := Handler{Logger: Logger{level: "prod"}, name: "Root"}
    http.ListenAndServe(":8080", h)
}

The Handler embeds Logger. It defines ServeHTTP to satisfy http.Handler. Inside ServeHTTP, it calls h.Info. The compiler promotes this to h.Logger.Info. The handler gets logging without boilerplate. You can pass h to http.ListenAndServe because Handler has the ServeHTTP method.

Convention aside: receiver names should be one or two letters matching the type. Use (l *Logger) and (h *Handler). Do not use (this *Logger) or (self *Logger). The community expects short names. It keeps the code scannable.

Pitfalls and conventions

Embedding creates a flat namespace for promotion. If you embed two structs with the same method name, the compiler complains. You get ambiguous selector s.Log. You have to call s.Logger.Log or s.Auditor.Log. You cannot call s.Log. Ambiguity is a design smell. If you have to disambiguate, your struct is doing too much. Split the responsibilities or use a regular field.

Copying semantics matter. If you embed a struct by value, the embedded field is copied when you copy the outer struct. If the embedded struct holds a mutex or a connection, copying breaks it. Embed pointers for shared state. If you embed *Logger, s.Logger is a pointer. Copying s copies the pointer, not the logger. This is usually what you want for stateful types.

Visibility rules apply to promotion. Unexported fields are not accessible from outside the package. If Logger has prefix string, code in another package cannot access s.prefix, even though Service embeds Logger. The promotion respects package boundaries.

Convention aside: gofmt is mandatory. Do not argue about indentation. Let the tool decide. Most editors run gofmt on save. Embedded fields are listed with just the type name, no field name. gofmt handles the formatting. Trust gofmt. Argue logic, not formatting.

Convention aside: "Accept interfaces, return structs." Embedding is about implementation details. You usually embed concrete types inside your own structs. You do not embed interfaces in the same way. You embed structs to implement interfaces. If you need flexibility, embed a struct that implements an interface, or use a field with an interface type.

The worst embedding bug is the one that copies a mutex. Always check if the embedded type holds shared state. If it does, embed a pointer.

When to embed and when not to

Use struct embedding when you want to reuse methods and fields from an existing type without writing boilerplate delegation. Use a regular field when you need to distinguish the inner type explicitly or when the inner type might change at runtime. Use an interface when you want to decouple the outer type from the concrete implementation of the inner behavior. Use composition with a field and explicit methods when you need to modify the behavior of the inner type before calling it.

Embed for reuse, fields for clarity, interfaces for flexibility.

Where to go next