Structural Patterns in Go

Adapter, Decorator, Facade

Implement Adapter, Decorator, and Facade patterns in Go by defining interfaces and creating wrapper structs that compose existing types.

The wrapper saves the day

You are building a service that talks to a payment gateway. The gateway library returns a Transaction struct with a Status field that is an integer. Your application uses a Status enum string. Every time you call the gateway, you write a switch statement to convert the integer to a string. Three months later, the gateway updates the integer codes. You have to find every switch statement in your codebase and update it. You need a way to wrap that external dependency so the rest of your code never sees the raw integers.

Go does not have classes. You cannot extend a class to add behavior. You cannot inherit from a base type. Go has interfaces and structs. Structural patterns like Adapter, Decorator, and Facade are not keywords in the language. They are shapes you make with interfaces and composition. Think of a power adapter. You have a laptop with a specific plug. The wall has a different socket. The adapter sits between them. It takes the wall's format and presents the laptop's format. The laptop does not care about the wall. The wall does not care about the laptop. The adapter handles the translation. In Go, a struct that wraps another type and implements an interface is that adapter.

The adapter pattern

An adapter translates an incompatible interface into one your code expects. You define the interface your application needs. You create a struct that wraps the external type. The struct implements your interface by calling the external type and converting the data.

Here is the simplest adapter. The application expects a PaymentProcessor. The legacy library provides a LegacyGateway with a different method signature.

// PaymentProcessor is the interface your application expects.
type PaymentProcessor interface {
    Charge(amount int) error
}

// LegacyGateway is the third-party library you cannot change.
type LegacyGateway struct{}

// Process uses an integer code for status instead of returning an error.
func (g *LegacyGateway) Process(amount int) int {
    return 200 // success code
}

// GatewayAdapter wraps LegacyGateway to match PaymentProcessor.
type GatewayAdapter struct {
    gateway *LegacyGateway
}

// Charge translates the app's request to the gateway's format.
func (a *GatewayAdapter) Charge(amount int) error {
    code := a.gateway.Process(amount)
    // Convert the integer code to an error if it indicates failure.
    if code != 200 {
        return fmt.Errorf("charge failed with code %d", code)
    }
    return nil
}

The compiler checks that GatewayAdapter implements PaymentProcessor. It looks at the methods. Charge exists. The signature matches. The compiler is happy. Go uses implicit interface satisfaction. There is no implements keyword. If a struct has all the methods of an interface, it implements the interface. This design makes refactoring safe. If you add a method to PaymentProcessor, the compiler immediately flags every adapter that is missing that method. You get cannot use adapter (variable of type GatewayAdapter) as PaymentProcessor value in argument: GatewayAdapter does not implement PaymentProcessor (missing Charge method). The error is verbose but precise. It tells you exactly which method is missing.

At runtime, you create the adapter. You pass it to a function that expects PaymentProcessor. The function calls Charge. The adapter calls Process. The translation happens inside the adapter. If the gateway changes its integer codes, you only update the adapter. The rest of the code remains untouched. The adapter protects your code from external changes.

The decorator pattern

A decorator adds behavior to an object without changing its interface. You wrap the object in a new struct. The wrapper implements the same interface. It adds extra work before or after delegating to the wrapped object. Decorators stack. You can wrap a logger with a prefix decorator, then wrap that with a timestamp decorator.

Here is a logger decorator. The base logger writes to standard output. The decorator adds a prefix to every message.

// Logger defines the behavior for writing messages.
type Logger interface {
    Log(msg string)
}

// StdLogger writes to standard output.
type StdLogger struct{}

// Log prints the message.
func (s *StdLogger) Log(msg string) {
    fmt.Println(msg)
}

// PrefixLogger wraps a Logger to add a prefix to every message.
type PrefixLogger struct {
    logger Logger
    prefix string
}

// Log adds the prefix before delegating to the wrapped logger.
func (p *PrefixLogger) Log(msg string) {
    p.logger.Log(p.prefix + ": " + msg)
}

The decorator accepts a Logger interface. It returns a PrefixLogger struct. This follows the Go mantra: accept interfaces, return structs. The decorator is flexible. It can wrap any type that implements Logger. It does not care about the concrete type. The caller creates the chain. prefix := &PrefixLogger{logger: &StdLogger{}, prefix: "APP"}. Then prefix.Log("started") prints APP: started.

Middleware in web servers is the most common decorator in Go. The http.Handler interface has a ServeHTTP method. Middleware wraps a handler to add logging, authentication, or rate limiting.

// Handler is the interface for HTTP handlers.
type Handler interface {
    ServeHTTP(w http.ResponseWriter, r *http.Request)
}

// LoggingMiddleware wraps a Handler to log requests.
type LoggingMiddleware struct {
    handler Handler
}

// ServeHTTP logs the request before passing it to the wrapped handler.
func (m *LoggingMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    m.handler.ServeHTTP(w, r)
    fmt.Printf("Request took %v\n", time.Since(start))
}

You can stack middlewares. logging := &LoggingMiddleware{handler: &AuthMiddleware{handler: &MyHandler{}}}. The request flows through the chain. Each middleware does its job and passes control to the next. Decorators are layers. Keep the chain short. Too many layers hide the truth and make debugging painful.

Field embedding and the shadow

Go has a feature called field embedding. You can put an interface or struct inside another struct without a field name. This promotes the methods of the embedded type. You can call the promoted methods directly on the outer struct.

// EmbeddedLogger uses field embedding to promote the Logger interface.
type EmbeddedLogger struct {
    Logger
    prefix string
}

// Log overrides the promoted Log method to add a prefix.
func (e *EmbeddedLogger) Log(msg string) {
    e.Logger.Log(e.prefix + ": " + msg)
}

Embedding is syntactic sugar. It generates a hidden field. It is not inheritance. You can still call the promoted method explicitly using the type name as the field name. e.Logger.Log calls the wrapped logger. If you define a method on the outer struct with the same name, it shadows the promoted method. This is subtle. Many developers confuse embedding with inheritance. It is composition with a shortcut. Use embedding when the wrapper is a thin pass-through and you want to expose most methods of the wrapped type. Use explicit fields when you need to control which methods are visible or when the wrapper changes the behavior significantly.

Pitfalls and compiler errors

Wrapping types can lead to infinite recursion if you are not careful. If your decorator calls itself instead of the wrapped object, the program panics with a stack overflow. The compiler cannot catch this logic error. You have to check the method implementation. Ensure the wrapper calls the underlying type, not this or the receiver.

Another pitfall is method explosion. If the interface has twenty methods, your adapter must implement all twenty. You cannot pick and choose. The compiler enforces this. If you miss one method, the compiler rejects the program. This is a feature. It forces you to think about the contract. If an interface has too many methods, it might be doing too much. Consider splitting the interface into smaller ones. Go favors small, focused interfaces. The io.Reader interface has one method. The io.Writer interface has one method. They compose easily. A large interface is hard to adapt and hard to mock.

Error handling is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. In an adapter, you often wrap errors to add context. return fmt.Errorf("gateway charge failed: %w", err). The %w verb wraps the error so callers can unwrap it later. This is standard practice. Do not swallow errors. Return them with context.

Trust gofmt. The formatting is decided by the tool, not the developer. Most editors run gofmt on save. Do not argue about indentation. Let the tool decide. The receiver name is usually one or two letters matching the type. Use (a *GatewayAdapter) or (p *PrefixLogger). Do not use (this *GatewayAdapter) or (self *GatewayAdapter). Public names start with a capital letter. Private names start lowercase. There are no keywords like public or private. Capitalization controls visibility.

When to use which pattern

Use an adapter when you need to translate an incompatible interface into one your code expects. Use a decorator when you want to add behavior to an object without changing its interface. Use a facade when a subsystem is too complex and you need a simplified entry point. Use plain composition when you just need to group related data and behavior without wrapping an interface. Use field embedding when you want to promote methods from a wrapped type and the interface is small. Use a direct struct when the type is the source of truth and no wrapping is needed.

A facade is a door. Make it easy to open. The facade hides the complexity of initialization and coordination. It does not add new behavior. It just provides a simpler interface. Context always goes as the first parameter. If your adapter or decorator takes a context, put it in the first argument. Functions that take a context should respect cancellation and deadlines. Pass the context through the chain. If the context is cancelled, stop the work. The worst goroutine bug is the one that never logs. Ensure your decorators log enough to diagnose failures.

Where to go next