Adapter Pattern — Junior¶
1. What the Adapter pattern actually is¶
You have a type that does the right thing but doesn't have the right shape. The codebase expects io.Writer, but the third-party library exposes a Send(buf []byte) method. The HTTP handler chain expects http.Handler, but your business logic is a func(context.Context, Request) (Response, error). The metrics package expects a prometheus.Collector, but your in-house metrics produce a different struct.
The Adapter pattern wraps the "wrong shape" type in a thin layer that translates between interfaces — without changing the underlying type. From the caller's perspective, the adapter looks like what they expected. From the wrapped type's perspective, nothing happened.
// Third-party library:
type Acme struct{}
func (a *Acme) Send(buf []byte) error { /* sends bytes */ }
// What our codebase wants:
// io.Writer with a Write([]byte) (int, error) method
// Adapter:
type AcmeWriter struct{ Acme *Acme }
func (w *AcmeWriter) Write(p []byte) (int, error) {
if err := w.Acme.Send(p); err != nil {
return 0, err
}
return len(p), nil
}
// Now Acme works wherever io.Writer is expected:
var w io.Writer = &AcmeWriter{Acme: &Acme{}}
io.Copy(w, source)
Three observations:
- The adapter is small — a struct and one method. That's by design. Adapters do the bare minimum to bridge interfaces.
- The wrapped type is untouched.
Acmedoesn't know it's being adapted. We didn't modify the third-party library. - The caller is also untouched. They use
io.Writerlike always; the adapter is invisible.
This pattern is one of the most-used in real Go code, especially around third-party libraries, legacy APIs, and stdlib interface boundaries. This file teaches:
- The minimum implementation and the three shapes (object, function, interface adapter).
- How Adapter differs from Decorator, Facade, and Proxy (they all wrap, but differently).
- When the adapter is the right call vs when you should change the underlying API.
- How
io.Reader,http.HandlerFunc, andbufio.NewReaderare all forms of Adapter (mostly).
2. Table of Contents¶
- What the Adapter pattern actually is
- Table of Contents
- Why this pattern is unavoidable
- The three Go shapes
- Object adapter — wrapping a struct
- Function adapter — wrapping a function
- Interface adapter — bridging two interfaces
- Adapter in the standard library
- A second worked example — legacy logging library
- Adapter vs Decorator vs Facade vs Proxy
- Common mistakes a junior makes
- Tricky points
- Quick test
- Cheat sheet
- What to learn next
3. Why this pattern is unavoidable¶
Go's interfaces are structural. If your type has the right methods, it satisfies any interface that lists them. You don't implements anything explicitly.
That sounds like adapters should be unnecessary. They're not. Three things keep adapters relevant:
-
You don't own the third-party type. Method names, signatures, receiver kinds — all are fixed by the upstream library. If your stdlib expects
Read(p []byte) (int, error)and the upstream gives youReadBytes(buf *Buffer), you can't add aReadmethod to a type you don't own. You wrap. -
Two interfaces serve the same purpose with different signatures.
io.Readerreads bytes. So doesbufio.Reader.ReadByte(). They share a concept but not a method set. To use one where the other is expected, you adapt. -
A function value needs to look like an interface. Go's "named function type with a method" trick is a function-to-interface adapter.
http.HandlerFuncadapts afunc(w, r)into anhttp.Handler. This is the most common adapter you'll write without realising.
Structural typing reduces the frequency of adapters in Go vs Java or C#, but doesn't eliminate them. Every codebase has a handful.
4. The three Go shapes¶
| Shape | Source | Target | Example |
|---|---|---|---|
| Object adapter | A struct with the wrong methods | Interface | Wrapping a third-party SDK as io.Writer |
| Function adapter | A function value | Interface | http.HandlerFunc adapting func to Handler |
| Interface adapter | One interface | Another interface | Wrapping io.ReadCloser to expose only io.Reader |
All three follow the same rule: the adapter has the target interface's methods, and those methods translate calls into the source type's API.
You'll see all three in any non-trivial Go codebase. Knowing which shape to pick takes some judgement; we cover it in §10.
5. Object adapter — wrapping a struct¶
The most common shape. Adapt a concrete type to satisfy an interface.
// Source — what we have:
package legacypkg
type SnailMailer struct{ smtp *SMTPClient }
func (m *SnailMailer) Deliver(to string, subject string, body string) error {
return m.smtp.Send(to, subject, body)
}
// Target — what we want:
package mail
type Mailer interface {
Send(ctx context.Context, msg Message) error
}
type Message struct {
To string
Subject string
Body string
}
// Adapter:
package adapters
type SnailMailAdapter struct {
Inner *legacypkg.SnailMailer
}
func (a *SnailMailAdapter) Send(ctx context.Context, msg mail.Message) error {
// Ignore ctx (legacy lib doesn't support cancellation)
// — or check ctx.Err() before delivering, as a courtesy.
if err := ctx.Err(); err != nil {
return err
}
return a.Inner.Deliver(msg.To, msg.Subject, msg.Body)
}
Usage:
var m mail.Mailer = &adapters.SnailMailAdapter{Inner: &legacypkg.SnailMailer{}}
m.Send(ctx, mail.Message{To: "alice@example.com", Subject: "Hi"})
The adapter:
- Has the target interface's method (
Send(ctx, msg)). - Delegates to the source's
Deliver(to, subject, body). - Translates the argument shape (a
Messagestruct into three positional arguments). - Inserts whatever the target expects but the source doesn't have (a context check).
That's the entire object adapter pattern. The translation logic stays small and focused; if it gets large, the adapter is probably masking a deeper mismatch (§11.4).
6. Function adapter — wrapping a function¶
Go's killer feature for adapting functions to interfaces: a named function type can have methods.
type Mailer interface {
Send(ctx context.Context, msg Message) error
}
// MailerFunc is a function value, but it has a Send method, so it satisfies Mailer.
type MailerFunc func(ctx context.Context, msg Message) error
func (f MailerFunc) Send(ctx context.Context, msg Message) error {
return f(ctx, msg)
}
Now any function with the right signature becomes a Mailer:
m := MailerFunc(func(ctx context.Context, msg Message) error {
return smtpClient.Send(msg.To, msg.Subject, msg.Body)
})
// m is a Mailer. Use it anywhere a Mailer is expected.
sendWelcomeEmail(m, user)
This is exactly what http.HandlerFunc does:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }
The adapter is the named type + the one-method receiver. Five lines of code. Pure boilerplate. But it lets every plain function with the right signature be an http.Handler.
When to use the function adapter shape¶
- The interface has one method (or one primary method).
- You want to support both function and struct implementations.
- You're publishing a library and care about how compactly callers can pass behaviour.
For interfaces with two or more meaningful methods, the function adapter doesn't fit — you'd need a struct holding multiple function fields. At that point use the object adapter shape (§5).
7. Interface adapter — bridging two interfaces¶
Sometimes you don't want all of an interface — just a slice. Or you want to use one interface as another.
// You have an io.ReadCloser (returned by http.Response.Body)
// You want to pass it to something that only needs io.Reader
// (so the consumer can't accidentally close it).
func readBody(r io.ReadCloser) {
onlyRead := io.Reader(r) // Go's interface assignment is free —
io.Copy(os.Stdout, onlyRead) // the consumer can't see Close()
}
When the conversion is more than a trivial assignment, write an explicit adapter:
// Wrap an io.Reader so it also implements io.Closer (as a no-op).
type readerToReadCloser struct{ r io.Reader }
func (rc readerToReadCloser) Read(p []byte) (int, error) { return rc.r.Read(p) }
func (rc readerToReadCloser) Close() error { return nil } // no-op
// Now an io.Reader can be passed where io.ReadCloser is needed.
func NewReadCloser(r io.Reader) io.ReadCloser { return readerToReadCloser{r: r} }
Real-world example: io.NopCloser(r io.Reader) io.ReadCloser does exactly this. It's in the stdlib because the need is common — you have a reader, the API wants a ReadCloser, and you have nothing to close.
When to use the interface adapter shape¶
- You're connecting two interfaces from different packages.
- One interface is a strict superset of the other (just expose the subset).
- One interface needs a method the other lacks (provide a no-op or sensible default).
8. Adapter in the standard library¶
You've used adapters in stdlib without thinking. A partial list:
| Where | What it adapts |
|---|---|
io.NopCloser(r io.Reader) io.ReadCloser | Adds a no-op Close() |
strings.NewReader(s string) *strings.Reader | Adapts string to io.Reader |
bytes.NewReader(b []byte) *bytes.Reader | Adapts []byte to io.Reader |
bufio.NewReader(r io.Reader) | Adapts a slow Reader to a buffered one (sort of) |
http.HandlerFunc | Adapts func to Handler |
http.FileServer(root FileSystem) | Adapts a FileSystem to an HTTP handler |
flag.Value interface | Adapts arbitrary types to flag's string parsing |
sort.Reverse(data Interface) Interface | Adapts a sort interface to its reverse |
httputil.NewSingleHostReverseProxy | Adapts an inner Handler to a proxy |
template.FuncMap | Adapts arbitrary funcs to template functions |
errors.Is and errors.As | Adapt typed errors to comparable / extractable values |
sort.Reverse is worth looking at:
// from sort
type reverse struct {
Interface
}
func (r reverse) Less(i, j int) bool { return r.Interface.Less(j, i) }
func Reverse(data Interface) Interface { return &reverse{data} }
That's an interface adapter in 5 lines. The reverse struct embeds sort.Interface, overrides Less, and the embedded methods (Len, Swap) pass through. The wrapped interface looks the same to the sort algorithm, but Less is inverted, so the sort runs backwards. Beautiful.
Read io.NopCloser's source while you're at it. It's a 7-line file. The pattern fits in your head.
9. A second worked example — legacy logging library¶
A real adapter you'll write at some point: bridging an old logger to a new one.
// Old code uses an in-house logger.
package legacy
type Logger interface {
Info(args ...any)
Error(format string, args ...any)
Debugf(format string, args ...any)
}
// New code uses log/slog.
package main
func handle(log *slog.Logger) {
log.Info("starting", "user", userID)
}
You want existing code that depends on legacy.Logger to actually log via slog. Adapter:
package adapters
import (
"fmt"
"log/slog"
)
type SlogLegacyAdapter struct {
Slog *slog.Logger
}
func (a *SlogLegacyAdapter) Info(args ...any) {
a.Slog.Info(fmt.Sprint(args...))
}
func (a *SlogLegacyAdapter) Error(format string, args ...any) {
a.Slog.Error(fmt.Sprintf(format, args...))
}
func (a *SlogLegacyAdapter) Debugf(format string, args ...any) {
a.Slog.Debug(fmt.Sprintf(format, args...))
}
Usage:
slogLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
legacyLogger := &adapters.SlogLegacyAdapter{Slog: slogLogger}
// Old code path keeps using legacy.Logger — but slog handles it now.
legacy.HandleRequest(req, legacyLogger)
Three observations:
- Conversion is lossy.
Info(args ...any)doesn't carry key-value semantics; we collapse it into a sprintf string. If you want structured logging, you can't always recover it from a legacy positional API. Some information is gone forever. - Inverse direction adapter is symmetric. If you want new code using
slogto delegate to a legacy logger (the other direction), write aLegacySlogAdapterwith the inverse translation. Both adapters share the same structure. - Adapters are a migration tool. Once all legacy callers move to
slog, the legacy interface and the adapter can be deleted. The adapter is temporary scaffolding, not a permanent abstraction.
10. Adapter vs Decorator vs Facade vs Proxy¶
Four GoF patterns that look alike. The differences matter once you've seen each.
Adapter¶
Converts one interface to another.
Two interfaces, different shapes; the adapter translates calls. The wrapped object provides the same behaviour; only the shape differs.
Decorator¶
Adds behaviour to an object while keeping the same interface.
One interface, the wrapper provides extra behaviour (logging, retry, caching). Same shape in, same shape out, more behaviour in the middle.
Facade¶
Provides a simpler interface to a complex subsystem.
The facade hides several types behind one. It's not just translating shape — it's collapsing multiple objects into a single, easier-to-use entry point.
Proxy¶
Controls access to an object.
Same interface as the wrapped object, but the proxy decides whether/how/when to forward calls. Remote proxies forward over a network; security proxies check permissions; virtual proxies lazy-load.
Decision table¶
| Goal | Pattern |
|---|---|
| Make a type satisfy a different interface | Adapter |
| Add cross-cutting behaviour (log, retry, cache) | Decorator |
| Hide a complex subsystem behind one type | Facade |
| Control access (remote, restricted, lazy) | Proxy |
In Go specifically, Adapter and Decorator are sometimes implementationally indistinguishable. The intent is what differs:
- Adapter changes the interface.
- Decorator enriches it.
If you can answer "which interface does this satisfy that the inner type doesn't?" — you're writing an Adapter. If the answer is "the same interface, plus extra behaviour" — Decorator.
11. Common mistakes a junior makes¶
11.1 Adding business logic to the adapter¶
func (a *SlogAdapter) Info(args ...any) {
if shouldFilter(args) { return } // BAD — business logic in adapter
a.Slog.Info(fmt.Sprint(args...))
}
The adapter's only job is to translate. Filtering is policy, not translation. Put it in a Decorator that wraps the adapter, or in the caller.
11.2 Adapter that owns too much¶
A single adapter hosting four collaborators isn't an adapter — it's a facade. Adapters are small. If you find yourself adding fields beyond the wrapped type, you've grown into Facade territory; rename and refactor.
11.3 Adapter that exposes the inner type¶
Callers can now reach around the adapter to call slog directly. The adapter's contract is now leaky — implementations that don't have an Inner() method can't substitute for it. Don't expose the wrapped type.
11.4 Writing an adapter when you should change the underlying API¶
// Adapter that does substantial work to bridge old to new
type MassiveAdapter struct{ Old *OldClient }
func (m *MassiveAdapter) DoNewThing(ctx context.Context, req NewReq) (NewResp, error) {
// 100 lines of translation, validation, error mapping, retry logic...
}
If the adapter is this large, the abstraction mismatch is structural. The right fix is to update the underlying API — either upstream the change to the old library, or wrap it once in a new domain type and ditch the legacy interface entirely. Don't keep the legacy interface alive forever with a 100-line adapter.
11.5 Returning a struct when the interface would do¶
// Anti-idiom
func NewSlogAdapter(s *slog.Logger) *SlogLegacyAdapter { return &SlogLegacyAdapter{Slog: s} }
// Caller now ties code to *SlogLegacyAdapter — defeats the adapter's purpose
The adapter exists to provide the target interface. Return that interface (or the adapter's pointer type, but document the type as an implementation detail). Don't let consumers depend on the adapter's concrete type.
12. Tricky points¶
12.1 The "nil source" trap¶
Constructors should validate:
func NewSlogLegacyAdapter(s *slog.Logger) *SlogLegacyAdapter {
if s == nil { panic("NewSlogLegacyAdapter: nil slog") }
return &SlogLegacyAdapter{Slog: s}
}
Or document that nil defaults to slog.Default(). Either is fine; just don't surprise the caller.
12.2 Adapting through type assertion¶
func adaptHandler(h interface{}) http.Handler {
if hh, ok := h.(http.Handler); ok { return hh }
if hf, ok := h.(func(http.ResponseWriter, *http.Request)); ok {
return http.HandlerFunc(hf)
}
panic("unsupported handler type")
}
This is "structural adapter at runtime" — based on what the value happens to be, build a different adapter. Useful for plugin systems; brittle for application code. Prefer explicit constructors.
12.3 Method-promoted adapter via embedding¶
type ReverseSorter struct {
sort.Interface
}
func (r ReverseSorter) Less(i, j int) bool { return r.Interface.Less(j, i) }
Embedding the interface promotes all its methods. The adapter overrides the one it cares about; the rest pass through. Elegant, but careful: if the inner interface gains a new method, your wrapper gets it automatically — sometimes good, sometimes bad.
12.4 The receiver kind matters for the function adapter¶
type MailerFunc func(ctx context.Context, msg Message) error
// Pointer receiver — doesn't work
func (f *MailerFunc) Send(ctx context.Context, msg Message) error {
return (*f)(ctx, msg)
}
// MailerFunc(f).Send(ctx, msg) — but *MailerFunc isn't comparable, can't use as map key, etc.
// Value receiver — idiomatic
func (f MailerFunc) Send(ctx context.Context, msg Message) error {
return f(ctx, msg)
}
Function values are small (a pair of pointers). Use a value receiver. Pointer receiver on a function type is unusual and usually a mistake.
13. Quick test¶
Q1. Which pattern fits?
You have a third-party PDF library with a method `Render(opts RenderOptions) ([]byte, error)`.
Your code uses an interface: `type Renderer interface { Render(ctx context.Context, doc Doc) ([]byte, error) }`.
You want the PDF library to satisfy `Renderer`.
Answer
Adapter. The shapes are different (`RenderOptions` vs `(ctx, doc)`), and your job is purely to translate. Write a small wrapper that maps `(ctx, doc)` into the library's `RenderOptions` (checking `ctx.Err()` first) and returns the result. Not Decorator — you're not adding behaviour. Not Facade — you're not hiding a subsystem. Not Proxy — you're not controlling access.Q2. Which shape is right?
// You want any function with this signature to act as a Charger:
type Charger interface { Charge(ctx context.Context, amount int) error }
Answer
Function adapter. Define `type ChargerFunc func(ctx context.Context, amount int) error` with a `Charge` method that invokes itself. Now any function with the right signature can be wrapped via `ChargerFunc(fn)` and used as a `Charger`. This is the `http.HandlerFunc` pattern. Five lines of code, dramatically improves the API.Q3. Spot the bug:
type LegacyAdapter struct{ Logger *legacy.Logger }
func (a *LegacyAdapter) Info(args ...any) {
if a.Logger == nil { return } // silently swallows
a.Logger.Info(args...)
}
Answer
The adapter silently no-ops when the logger is nil. Callers think they're logging, but nothing happens. Bug: in production, you'd lose log entries with no warning. Fix: validate in the constructor (`if logger == nil { panic(...) }`) or use a sensible default (`legacy.Default`). Don't silently discard.14. Cheat sheet¶
| What | How |
|---|---|
| The adapter has the target interface's methods | Each method translates and delegates |
| Object adapter | Struct holds a source, methods translate |
| Function adapter | Named func type with a method satisfying the interface |
| Interface adapter | Struct wraps one interface to expose another |
| Construction | Validate nil source; return concrete type (callers depend on interface) |
| Receiver kind | Value receiver for function adapters; pointer for object adapters with state |
| Translation logic | Tiny — args-shuffling, missing-method shims |
| Anti-patterns | Adding business logic, exposing the inner type, owning multiple collaborators |
| Lifecycle | Often temporary — for migration or third-party bridging |
| Related patterns | Decorator (same iface, more behaviour); Facade (collapses many); Proxy (controls access) |
15. What to learn next¶
- middle.md — Two-way adapters, generic adapter helpers, multi-interface adapter, adapters in dependency injection wiring, testing adapters.
- ../04-decorator-pattern/ — Re-read; note where you'd use Adapter vs Decorator in real cases.
- ../10-facade-pattern/ — When the adapter grows up.
- ../11-proxy-pattern/ — Cousins of Adapter; remote proxies are adapters that cross a network.
Adapter is the workhorse of integration. Every library boundary, every legacy migration, every "I have a func but I need an interface" call site is an adapter. Spotting them early — and keeping them small — is what separates clean Go code from XxxAdapterImpl chaos.