Adapter Pattern — Senior¶
1. What this level covers¶
Junior taught the shape; middle covered the variants and the production traps. Senior is about architecture:
- Designing adapter APIs that other teams will write against (libraries).
- Evolving adapters across major version boundaries without breaking consumers.
- Hexagonal architecture (ports and adapters) applied at the Go service level.
- Real-world adapter ecosystems:
database/sqldrivers,http.RoundTripper, gRPC ↔ HTTP, OpenTelemetry instrumentation. - Anti-patterns that show up only at scale: god adapters, adapter chains, leaky abstractions through type assertions.
- Concurrency in adapters that must serve thousands of QPS.
- Postmortems — real bugs traced to adapter mistakes.
- Cross-language: what Go-style structural adapters look like in Java/Rust/C#.
The decisions at this level are usually irreversible. A bad adapter API design at v1 becomes infrastructure for years.
2. Table of Contents¶
- What this level covers
- Table of Contents
- Designing adapter APIs for downstream consumers
- Evolution: adapters across major versions
- Hexagonal architecture in Go
- Real ecosystem: database/sql drivers
- Real ecosystem: http.RoundTripper
- Real ecosystem: gRPC ↔ HTTP
- Real ecosystem: OpenTelemetry instrumentation
- Anti-patterns at scale
- Concurrency in adapters
- Performance — when adapters cost
- Contract testing across packages
- Postmortems
- Cross-language comparison
- Common senior mistakes
- Tricky questions
- Cheat sheet
- Further reading
3. Designing adapter APIs for downstream consumers¶
When you publish a library, the interfaces you expose become adapter contracts — every consumer writes an adapter to bridge their world to yours. The shape of those contracts determines how painful that adapter is to write.
3.1 Small interfaces win¶
// Hard to adapt — 12 methods, many irrelevant to most consumers
type Store interface {
Get(...) (Item, error)
Put(...) error
Delete(...) error
Watch(...) error
Snapshot() ([]byte, error)
Restore([]byte) error
/* ... 6 more ... */
}
A consumer who only wants Get and Put writes an adapter implementing 12 methods, 10 of them stubs. The stubs are a smell — they lie about the underlying capability. The library accumulates "not supported" implementations forever.
// Easy to adapt — segregated
type Reader interface { Get(...) (Item, error) }
type Writer interface { Put(...) error; Delete(...) error }
type Watcher interface { Watch(...) error }
type Backupable interface { Snapshot() ([]byte, error); Restore([]byte) error }
Each consumer adapts only what they need. Adapters are tiny. Capability is declared by which interfaces the adapter implements.
3.2 Return concrete types from constructors¶
// Anti-idiom — library returns interface
func NewStore() Store { return &realStore{} }
// Idiomatic — library returns concrete; consumers use interface in their parameter types
func NewStore() *Store { return &Store{} }
A consumer who wants additional methods (introspection, debug) loses access if the constructor returns an interface. Returning concrete preserves consumer optionality.
3.3 Provide adapter helpers (the HandlerFunc trick)¶
For single-method interfaces, always provide the named-func-type adapter:
type Store interface { Get(id string) (Item, error) }
type StoreFunc func(id string) (Item, error)
func (f StoreFunc) Get(id string) (Item, error) { return f(id) }
Now consumers can pass either a struct or a plain function. This is one of the highest-leverage API design decisions in Go: five lines of boilerplate, dramatically improved ergonomics.
4. Evolution: adapters across major versions¶
Adapters are contracts — they constrain how you can evolve the interface.
4.1 Adding a method is breaking¶
// v1.0
type Store interface { Get(id string) (Item, error) }
// v1.1 — ADDS a method
type Store interface {
Get(id string) (Item, error)
GetWithMeta(id string) (Item, Meta, error) // new
}
Every adapter implementing v1.0 silently fails to implement v1.1. Type assertions and direct uses still compile (Go's structural typing), but the interface check breaks. This is a v2.0 change, not v1.1.
Fix: a new interface for the new method:
type Store interface { Get(id string) (Item, error) }
type StoreWithMeta interface {
Store
GetWithMeta(id string) (Item, Meta, error)
}
Old code uses Store. New code requiring metadata uses StoreWithMeta. The library checks at runtime:
if swm, ok := s.(StoreWithMeta); ok {
item, meta, err := swm.GetWithMeta(id)
// use the rich path
} else {
item, err := s.Get(id)
// use the legacy path
}
This is the optional interface pattern. net/http uses it heavily: http.Hijacker, http.Flusher, http.Pusher are all optional interfaces that ResponseWriter implementations may satisfy.
4.2 Removing a method is breaking¶
Obvious, but worth stating. Once a method is in an interface, removing it breaks every adapter. Even if no one is using it.
4.3 Changing a method signature is breaking¶
// v1
Get(id string) (Item, error)
// v2 — adds context
Get(ctx context.Context, id string) (Item, error)
Every adapter must be rewritten. The migration plan is usually:
- Add
GetCtx(new signature) as an additional method via an optional interface. - Wait for consumers to migrate.
- In v2.0, drop
Getand renameGetCtxback toGet.
The two-phase migration spans a major version. Plan for years, not weeks.
5. Hexagonal architecture in Go¶
The "ports and adapters" architecture, applied at the Go service level:
5.1 Where each piece lives¶
- Domain package (
order/,user/,billing/) — declares the ports (interfaces it needs). Has no external imports beyond stdlib. - Adapter packages (
adapters/postgres,adapters/stripe) — implement the ports. Each imports one external dependency. - Composition root (
cmd/server/main.go) — wires adapters to domain services.
5.2 Why it pays off¶
- Domain code is testable without spinning up Postgres, Stripe, or AWS.
- Replacing Stripe with PayPal is a
main.gochange. Domain code doesn't move. - Each adapter is small (the thin-adapter rule from middle §4). All complexity stays in domain.
5.3 Why it sometimes doesn't¶
- For small apps (<5k LOC), the overhead exceeds the benefit.
- For exploratory code, it slows iteration.
- The discipline only works if everyone on the team follows it. One person reaching directly into
*sql.DBfrom domain code unravels months of design.
Use it for core services that you expect to maintain for years. Skip it for scripts and prototypes.
6. Real ecosystem: database/sql drivers¶
database/sql is the canonical multi-method adapter ecosystem in Go.
// from database/sql/driver
type Driver interface {
Open(name string) (Conn, error)
}
type Conn interface {
Prepare(query string) (Stmt, error)
Close() error
Begin() (Tx, error)
}
type Stmt interface {
Close() error
NumInput() int
Exec(args []Value) (Result, error)
Query(args []Value) (Rows, error)
}
// ... and more
Each database driver (pq, pgx, mysql, sqlite3) is an adapter implementing these interfaces. The database/sql package speaks to drivers through this interface — it doesn't import any specific driver. Drivers self-register via sql.Register("postgres", &pq.Driver{}) in their init().
Two architectural lessons:
6.1 The "interfaces upstream of implementations" rule¶
database/sql/driver is in the standard library — drivers depend on it, not vice versa. This is essential for the architecture: stdlib can't depend on third-party drivers.
In your own code, the equivalent rule: interfaces live in the consumer's package, adapters live in the implementation's package. The implementation package depends on the consumer, never the other way.
6.2 Backwards-compatible evolution via optional interfaces¶
database/sql/driver originally had:
Then context was added everywhere in Go 1.8 (2017). Rather than break every driver, new methods were introduced as optional interfaces:
type ConnPrepareContext interface {
PrepareContext(ctx context.Context, query string) (Stmt, error)
}
type ConnBeginTx interface {
BeginTx(ctx context.Context, opts TxOptions) (Tx, error)
}
database/sql checks at runtime: if the driver implements ConnPrepareContext, use it; otherwise fall back to Prepare. Drivers can adopt context support at their own pace. v1.0 drivers from 2014 still work in 2026.
This is the gold standard for adapter API evolution. Study it.
7. Real ecosystem: http.RoundTripper¶
A one-method interface. Every HTTP transport adapter (real network, mock, recording, retrying, tracing) implements RoundTripper. They compose by wrapping:
var rt http.RoundTripper = http.DefaultTransport
rt = &retryRoundTripper{Inner: rt, attempts: 3}
rt = &tracingRoundTripper{Inner: rt, tracer: tr}
rt = &loggingRoundTripper{Inner: rt, log: log.Default()}
client := &http.Client{Transport: rt}
RoundTripper is also an adapter target. Library code that needs to fake HTTP calls writes a custom RoundTripper:
type fakeRoundTripper struct{ resp *http.Response }
func (f *fakeRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
return f.resp, nil
}
Architectural payoff: HTTP testing works without any network. The adapter mocks at the transport layer, so http.Client.Do works exactly as it does in production.
OpenTelemetry's otelhttp.NewTransport(rt) wraps any RoundTripper with span creation. Prometheus' promhttp.RoundTripperFunc wraps with metrics. The whole ecosystem stacks on this one interface.
8. Real ecosystem: gRPC ↔ HTTP¶
gRPC and HTTP are different protocols. Adapters bridge them.
8.1 grpc-gateway¶
grpc-gateway reads protobuf service definitions and generates an HTTP/JSON adapter. The adapter:
- Accepts HTTP requests with JSON bodies.
- Translates each to a gRPC call against the same service.
- Translates the gRPC response back to JSON.
For each gRPC method, the generator produces a small HTTP handler that's effectively an adapter. The user writes the gRPC service once; both HTTP/JSON and gRPC/protobuf clients work.
8.2 connect-go¶
Connect (from Buf) is a single library that serves both gRPC and HTTP/JSON from the same code. Its core type is an HTTP handler that internally adapts to a typed protobuf-style method call. Adapter pattern, applied to the protocol layer.
8.3 Lessons¶
- When you need to support multiple protocols, one logical service with N adapters is cleaner than N parallel implementations.
- The adapters can be code-generated — saves writing the translation boilerplate by hand.
- Protocol-level adapters are usually thin: header conversion, body serialisation, status code mapping. Business logic stays in the underlying service.
9. Real ecosystem: OpenTelemetry instrumentation¶
OpenTelemetry's Go instrumentation libraries (otelhttp, otelgrpc, otelsql, otelfiber) are all adapters that wrap existing libraries to emit telemetry. Each wraps a standard interface (http.Handler, grpc.UnaryServerInterceptor, sql.Driver, etc.) and produces the same interface with spans/metrics added.
otelhttp example:
handler := http.HandlerFunc(myHandler)
wrapped := otelhttp.NewHandler(handler, "my-service")
// wrapped is still http.Handler, but every request now produces a span
The architectural insight: when an ecosystem standardises on small interfaces (http.Handler, RoundTripper, sql.Driver), instrumentation can be cross-cutting and non-invasive. You don't modify your application code; you add an adapter at the edge.
This is only possible because the underlying interfaces are small and stable. Adapter-friendly APIs unlock cross-cutting tooling.
10. Anti-patterns at scale¶
10.1 The god adapter¶
An adapter that wraps a single dependency but accumulates dozens of methods and several collaborators over time. By year two, it's the only thing in the project that knows how to talk to Postgres, and it has 2000 lines.
Fix: split by responsibility. A UserStore adapter, a BillingStore adapter, an EventStore adapter — each thin. The fact that all three talk to Postgres is incidental.
10.2 Adapter chains masquerading as architecture¶
// Three layers of adapters between the caller and the implementation
type FrontendAdapter struct{ Inner BackendAdapter }
type BackendAdapter struct{ Inner DBAdapter }
type DBAdapter struct{ Inner *sql.DB }
Each adapter is thin (good), but the chain adds three layers of indirection for no reason. Usually the result of organic growth: someone added a "shim" between every layer "in case it changes".
Fix: collapse adjacent thin adapters into one. Three thin adapters in series rarely justify their existence.
10.3 Type assertions defeating the abstraction¶
func (s *Service) Process(repo Repo) {
if sqlrepo, ok := repo.(*SQLRepo); ok {
// bypass the interface for "optimized" path
sqlrepo.bulkInsert(...)
} else {
for _, item := range items { repo.Insert(item) }
}
}
The service now depends on *SQLRepo existing. Replacing the SQL repo with a Mongo one breaks the optimization. The abstraction leaks.
Fix: declare an optional interface (BulkInserter) and assert against that. Multiple implementations can satisfy BulkInserter independently.
10.4 Adapters that hide errors¶
func (a *Adapter) Save(ctx context.Context, e Entity) error {
err := a.inner.Save(e)
if err != nil {
log.Printf("Adapter.Save: %v", err)
return nil // !
}
return nil
}
The error is logged but not propagated. Upstream code thinks the save succeeded. Months later, data inconsistency surfaces. The trace leads to this adapter.
Fix: always propagate. Logging and returning is fine. Swallowing is never fine.
10.5 Adapter constructors that perform I/O¶
func NewStripeAdapter(apiKey string) (*StripeAdapter, error) {
client := stripe.New(apiKey)
if err := client.Ping(); err != nil { return nil, err } // network call in constructor
return &StripeAdapter{client: client}, nil
}
main() now blocks on Stripe's API before the service can start. If Stripe is down, the service won't boot. If Stripe times out, startup hangs.
Fix: defer the check to first use, or expose a separate Verify(ctx) method. Constructors should not do I/O unless the user opts in.
11. Concurrency in adapters¶
Adapters are usually shared across goroutines. Three concerns:
11.1 Wrap concurrent-safe sources¶
type DBAdapter struct{ db *sql.DB }
func (a *DBAdapter) Get(ctx context.Context, id string) (User, error) {
var u User
err := a.db.QueryRowContext(ctx, "SELECT id, name FROM users WHERE id=$1", id).Scan(&u.ID, &u.Name)
return u, err
}
*sql.DB is goroutine-safe. The adapter is too — no shared mutable state. Stateless adapters are by far the easiest.
11.2 Wrap non-concurrent sources¶
type LegacyClient struct{ /* NOT concurrent-safe */ }
type Adapter struct {
mu sync.Mutex
client *LegacyClient
}
func (a *Adapter) Charge(ctx context.Context, amount int) error {
a.mu.Lock()
defer a.mu.Unlock()
return a.client.Charge(amount)
}
The mutex serialises all calls. Fine for a low-QPS adapter; a bottleneck for hot-path. Better: a pool of clients.
11.3 Lazy initialisation¶
type Adapter struct {
once sync.Once
client *Client
apiKey string
}
func (a *Adapter) lazy() *Client {
a.once.Do(func() { a.client = NewClient(a.apiKey) })
return a.client
}
sync.Once ensures one initialisation under concurrency. Useful when the underlying client is expensive to construct and may not always be used.
12. Performance — when adapters cost¶
Adapters add one method call per operation. Numbers:
BenchmarkDirect-8 500000000 2.1 ns/op 0 B/op
BenchmarkAdapter-8 400000000 2.6 ns/op 0 B/op
BenchmarkAdapterInterface-8 300000000 3.4 ns/op 0 B/op
BenchmarkThreeAdapterChain-8 200000000 4.2 ns/op 0 B/op
BenchmarkGenericAdapter-8 300000000 3.6 ns/op 0 B/op
Almost never an issue. Where it shows up:
12.1 Escape analysis at interface conversion¶
func process(items []Item) {
for _, item := range items {
var c Charger = &chargerAdapter{Item: item}
c.Charge()
}
}
&chargerAdapter{Item: item} allocates on the heap once per iteration if the compiler can't prove the interface stays in the frame. At 1M iterations, that's 1M allocations.
Fix: keep one adapter, mutate (if safe) or pre-allocate:
adapter := &chargerAdapter{}
for _, item := range items {
adapter.Item = item
var c Charger = adapter
c.Charge()
}
12.2 PGO devirtualization¶
Go 1.21+ profile-guided optimization can devirtualize hot interface calls when the profile shows the same concrete type dominates. For a long-running service with consistent adapter types, this can reclaim most of the dispatch cost.
To benefit: collect a CPU profile in production (go tool pprof), feed it to go build -pgo=cpu.pprof. Hot-path adapter calls get inlined.
12.3 The "interface in inner loop" smell¶
If you're constructing-and-discarding adapters inside a tight loop, refactor to construct once. The allocation cost dominates the dispatch cost.
13. Contract testing across packages¶
When package A defines an interface and package B provides an adapter, who tests the adapter satisfies the interface?
13.1 The compile-time check¶
Forces the compiler to verify. If the adapter misses a method or has wrong signatures, the build breaks.
13.2 The behaviour test¶
func runIfaceContract(t *testing.T, x Iface) {
t.Helper()
// exercise every method with expected inputs
err := x.Do(ctx, validInput)
if err != nil { t.Fatalf("Do(valid): %v", err) }
// ... etc
}
Each adapter implementation calls the contract test with its own instance. The contract enforces behaviour, not just signatures.
13.3 Liskov substitutability¶
Every adapter for the same interface should behave identically for the same input. Different implementations may have different capabilities (some support batch, some don't), expressed via optional interfaces — but for the methods they do implement, behaviour is identical.
Use property-based testing (testing/quick or gopter) to fuzz the same inputs against multiple adapters. Divergent behaviour = bug somewhere.
14. Postmortems¶
14.1 The case of the silenced context¶
A legacy SMS gateway had no context support. The adapter dropped ctx silently — Send(ctx, msg) ignored cancellation. During an incident, retry storms hit the gateway because the application thought slow sends could be cancelled but they couldn't. Eventually the gateway rate-limited the IP.
Fix: the adapter now spawns a goroutine that races the legacy call against ctx.Done(). If context cancels first, the adapter returns; the goroutine still runs (in vain). Documentation states this clearly. The application reduced retry aggressiveness based on the documented limitation.
Lesson: silently dropping context is a correctness bug, not just an aesthetic one.
14.2 The leaky adapter¶
A database/sql adapter exposed an Underlying() *sql.DB method "for testing". Three years later, a refactor changed *sql.DB to a custom connection pool. Two hundred call sites had reached past the adapter via Underlying(). The migration took six months.
Lesson: never expose the inner type. If tests need access, structure the test to inject the inner type before construction; don't extract it after.
14.3 The shared state adapter¶
A logger adapter held a []string buffer for batching. Two goroutines using the same adapter raced on append. Lost log lines. Took two months to find because the loss only appeared under load.
Fix: the adapter now uses sync.Mutex around the buffer access. Or, better, the inner logger is replaced with one that's already concurrent-safe.
Lesson: stateful adapters need the same concurrency analysis as any other shared object.
14.4 The optional interface that wasn't checked¶
A service code path expected Cache.Invalidate(). The adapter being used didn't implement Invalidator (an optional interface). The code did a type assertion: c.(Invalidator).Invalidate(). Production: nil pointer dereference, service crash.
Fix: always check the assertion:
Lesson: type assertions without ok check are landmines. Linters catch some; vigilance catches the rest.
15. Cross-language comparison¶
| Language | Adapter mechanism | Notes |
|---|---|---|
| Java | Adapter class implementing target interface, holding source as field | Explicit implements; requires recompilation when interface changes |
| C# | Extension methods + explicit adapter classes | Extension methods bridge some shape differences without a wrapper class |
| Rust | impl Trait for Source via newtype pattern | Compile-time check; no runtime dispatch unless dyn Trait |
| Kotlin | Delegation (class A : I by source) | Built-in language feature for "wrap and forward" |
| Scala | Implicit conversions, type classes | Adapter can be invisible at the call site |
| TypeScript | Structural typing like Go | Often no adapter needed; matches Go's situation |
| Python | Duck typing | No adapter; method existence checked at call time |
Go's structural typing reduces the frequency of adapters but doesn't eliminate them. Compared to Java, you write fewer adapters. Compared to Kotlin's delegation, you write more boilerplate.
The trade-off: Go's adapters are visible (you can find them by searching for the type name), whereas Kotlin's by delegation can hide significant amounts of forwarding logic. Verbosity helps debuggability.
16. Common senior mistakes¶
16.1 Designing for the adapter's first user, not future ones¶
The first consumer of your library shapes the interfaces you publish. If they only need read access, you design a one-method Reader. Then a second consumer needs writes — you add Write to Reader "for simplicity". Now read-only adapters must stub Write.
Imagine the next ten consumers before publishing.
16.2 Exposing the adapter type as a contract¶
package adapters
type SlogAdapter struct{ ... }
// Caller code:
var x *adapters.SlogAdapter = adapters.New(slogger)
If SlogAdapter is exported and used as a type, you can never refactor it without breaking callers. Keep adapter types unexported; expose only the constructor and target interface.
16.3 Adapters that "almost" satisfy the contract¶
func (a *Adapter) Send(ctx context.Context, msg Message) error {
if ctx.Err() != nil { return nil } // !
return a.inner.Deliver(...)
}
The adapter returns nil when the context is cancelled — silently appearing to succeed. The contract says "respect cancellation by returning an error". The adapter violates Liskov.
16.4 Letting test adapters drift from production behaviour¶
Test fakes are also adapters (to the same interface). When the real adapter handles a new edge case, the fake must too. If they drift, tests pass against the fake and break against production.
Solution: contract tests (§13) that exercise both fake and real adapters. Drift gets caught at CI time.
16.5 Skipping the compile-time interface check¶
type StoreAdapter struct{ db *sql.DB }
func (s *StoreAdapter) Get(...) (Item, error) { /* ... */ }
// No `var _ Store = (*StoreAdapter)(nil)`
A typo in the method name produces an adapter that doesn't satisfy Store. The next caller using StoreAdapter as Store gets a confusing compile error far from the bug. Add the check at the adapter's declaration site.
16.6 Treating adapters as forever¶
Adapters are scaffolding. Most should be deleted within a year or two — when the legacy library is gone, when the migration completes, when the interface stabilises. If you have adapters older than three years, audit them: are they still needed?
17. Tricky questions¶
Q1. When is the right time to write an adapter vs change the underlying API?
Answer
Write an adapter when: - You don't own the underlying API (third-party library). - The change is large and you need a transition period. - Multiple consumers can't all migrate at once. Change the API when: - You own both ends. - The adapter would be substantial (>50 lines). - The mismatch is structural, not just shape. Rule of thumb: adapters >100 lines suggest the underlying API needs work.Q2. A consumer wants to use methods specific to the wrapped type (e.g., *sql.DB-specific methods). How do you support that without exposing the inner type?
Answer
Two options: 1. **Add the methods to your interface** if they're broadly applicable. The interface widens; consumers can call them through the abstraction. 2. **Optional interfaces.** Define a separate interface (`BulkInserter`) that some implementations satisfy. Consumers type-assert: `if bi, ok := s.(BulkInserter); ok { ... }`. Implementations that don't support it are transparently skipped. Never expose the concrete type. The whole abstraction collapses.Q3. You have a 20-method interface. How do you decide whether to use embedding or explicit forwarding in an adapter?
Answer
**Embedding** when: - Most methods pass through unchanged. - The wrapped type's method set is stable. - You don't mind future additions to the inner interface being silently inherited. **Explicit forwarding** when: - Each method needs translation (different argument shapes). - The inner interface may grow and you want compile errors when it does. - You want the adapter to be readable as a list of explicit translations. For a 20-method interface where 18 pass through and 2 translate, embed and override the 2. For a 20-method interface where all 20 translate, write all 20 explicitly — embedding wouldn't save anything.Q4. A library you depend on changed an interface in a backwards-incompatible way. Your adapter no longer compiles. How do you minimise damage to your consumers?
Answer
1. *Don't* propagate the breakage. Pin the library at the old version while planning. 2. Write a *transition adapter* that exposes the old interface to your consumers but uses the new library internally. Your consumers don't see the breakage. 3. Plan a major version bump where you expose the new interface and consumers migrate. 4. Eventually delete the transition adapter. The transition adapter is a *cost*. It exists to give consumers time. If you don't have many consumers, just bump major and break — the transition adapter is overhead they didn't need.Q5. When should an adapter be a public type vs a function returning the interface?
Answer
**Function returning interface** (default): - Consumers depend on the interface, not the adapter. - You can change the adapter's internals freely. - Testing happens against the interface. **Public adapter type**: - Almost never. Exceptions: - The adapter has methods *beyond* the interface that consumers need (and you've decided those extras are part of the contract). - You're publishing a library where callers want to embed the adapter into their own types. The default is "private adapter, public constructor returning interface". Override only with strong reason.18. Cheat sheet¶
| Decision | Choose |
|---|---|
| Adapter or Decorator? | Adapter changes interface; Decorator preserves it |
| Adapter or Facade? | Adapter is 1:1; Facade hides many objects |
| Library evolution: add method | Optional interface, never widen the existing one |
| Adapter exposure | Private type, public constructor returning interface |
| Stateful adapter concurrency | Stateless if possible; mutex if not; pool if hot |
| Test for satisfaction | var _ Iface = (*Adapter)(nil) at declaration |
| Behaviour test | Shared contract test running against every implementation |
| Context dropped in inner library | Document loudly; race goroutines if cancellation matters |
| Inner type access for tests | Inject before construction; never expose after |
| Constructor I/O | Defer to first use or separate Verify(ctx) |
19. Further reading¶
- Go blog: "Strings, bytes, runes and characters in Go" — illustrates adapter design across types
- Go blog: "Errors are values" — adapter design for error wrapping
net/httpsource:server.go—HandlerFuncis the canonical adapterdatabase/sql/driver/driver.go— optional interface pattern at the stdlib levelgolang.org/x/net/http2— RoundTripper adapter for HTTP/2google.golang.org/grpc— interceptor and credentials are adapter-drivengo.opentelemetry.io/otel/instrumentation/...— instrumentation as cross-cutting adapter- Alistair Cockburn, "Hexagonal Architecture" — the architectural foundation
- "Domain-Driven Design" (Eric Evans), chapter on adapters — the strategic side
Adapter pattern in Go is unglamorous but load-bearing. The architectural decisions you make about which interfaces to publish, where to place adapters, and when to delete them shape the long-term maintainability of any service that integrates with the outside world.