Go Embedding & Dependency Injection
Embedding & Composition
What: Placing a type name (not a named field) inside a struct or interface promotes its methods/fields to the outer type. Composition, not inheritance — there is no virtual dispatch back to the outer type.
Syntax/Signature: type S struct { io.Reader; mu sync.Mutex } · type RW interface { io.Reader; io.Writer }.
type ReadCloser interface {
io.Reader
io.Closer
}
type Server struct {
*slog.Logger // promoted: s.Info(...)
mu sync.Mutex // NOT embedded — don't expose Lock/Unlock
handlers map[string]http.Handler
}
func NewServer(l *slog.Logger) *Server {
return &Server{Logger: l, handlers: map[string]http.Handler{}}
}
How it works under the hood: Promotion is compile-time sugar: the compiler generates wrapper methods that forward to the embedded field at a fixed offset. The embedded type's methods count toward the outer type's method set (see [[Go Interface Internals]] — depth-1 shallower names shadow deeper ones; same-depth conflicts must be resolved explicitly). No dynamic dispatch: if Base.Run calls Base.step, embedding and "overriding" step on the outer type does not redirect that inner call.
Interview angle: "How do you mock a huge third-party interface for tests?" — embed the interface in a stub struct, implement only what the test calls; unimplemented promoted methods panic with nil-pointer if hit (a feature: fails loudly).
Gotchas / common wrong answers:
- Expecting inheritance-style override polymorphism — the "fragile base class" call stays on the inner type.
- Embedding
sync.Mutexin an exported struct leaksLock/Unlockinto the public API (and makes the struct Copy-unsafe silently satisfysync.Locker) — see [[Go Sync Primitives]]. - Embedded nil pointer (
*slog.Loggernever set) ⇒ every promoted call panics. - JSON: fields of an embedded struct are flattened into the parent object unless the embed has a tag/name.
Trade-offs / when NOT to use: Prefer a named field when you don't want method-set pollution or accidental interface satisfaction. Embedding is API design, not a keystroke-saver.
Dependency Injection Idioms ("Accept Interfaces, Return Structs")
What: Go DI is manual constructor injection: consumers declare small interfaces describing what they need; constructors accept those and return concrete types. No framework required.
Syntax/Signature: interface defined next to the consumer, not the implementation.
// package checkout — the CONSUMER defines the seam it needs:
type ChargeClient interface {
Charge(ctx context.Context, cents int64) error
}
type Service struct {
charger ChargeClient
logger *slog.Logger
}
func NewService(c ChargeClient, l *slog.Logger) *Service {
return &Service{charger: c, logger: l}
}
// package stripe returns a concrete struct — it never imports checkout,
// yet *stripe.Client satisfies checkout.ChargeClient structurally.
How it works under the hood: Structural typing is what makes consumer-side interfaces possible: stripe.Client satisfies checkout.ChargeClient with zero coupling (see [[Go Interface Internals]]). All wiring happens in main (the composition root); the dependency graph is explicit code, checked by the compiler. wire (compile-time codegen) or fx (runtime, reflection) exist for very large graphs, but hand-wiring is the default idiom and the expected interview answer.
Interview angle: "How do you test code that hits Stripe/DB?" — inject a consumer-defined interface, hand-write a fake (Go devs prefer fakes over mock frameworks; gomock exists but hand-rolled fakes read better). Follow-up: "Why not define the interface in the stripe package?" — producer-side interfaces grow to mirror the whole client (unmockable), and every consumer inherits methods it doesn't use — violates interface segregation (see [[SOLID Principles]]), which Go gets for free by keeping interfaces small and local.
Gotchas / common wrong answers:
- Giant interfaces (
UserStorewith 15 methods) — split per consumer; 1–3 methods is idiomatic (io.Readeris one method). - Returning interfaces from constructors "for flexibility" — hides methods, breaks type assertions, and makes adding methods a breaking change. Return
*Service. - Global singletons /
init()-time wiring instead of passing deps — untestable and hides the graph. - Storing deps in
context.Context— context is for request-scoped values and cancellation, not DI (see [[Go Context & Cancellation]]).
Trade-offs / when NOT to use: Don't interface-wrap types you never substitute (e.g., *slog.Logger is fine concrete). Every seam is indirection; add them where a test or a second implementation actually exists.
Interview Drills
- Why does embedding not give you inheritance-style method overriding? — Promotion is purely compile-time forwarding to a fixed field offset — there's no virtual dispatch table the way class-based inheritance has. If an embedded type's method internally calls another method on itself, that inner call stays bound to the embedded type; "overriding" that inner method on the outer type doesn't redirect it, because there was never a dynamic lookup to intercept.
- Why should the interface for "talking to Stripe" be defined in the
checkoutpackage (the consumer) rather than thestripepackage (the producer)? — A producer-side interface tends to grow to mirror the entire client's surface (hard to mock, violates interface segregation), and every consumer would depend on methods it doesn't actually use. A consumer-defined interface stays minimal (exactly what that one consumer needs), and structural typing means the producer's concrete type satisfies it without the producer package ever needing to import or know about the consumer. - What's wrong with a constructor that returns an interface type instead of a concrete struct pointer, "for flexibility"? — It hides any methods not on the interface (callers can't access them even if they need to later), breaks type assertions callers might reasonably want, and turns adding a new method to the concrete type into a potentially breaking change for anyone who type-asserted against the interface — Go idiom is "accept interfaces, return structs" specifically to avoid this self-inflicted rigidity.
Cross-links
[[Go Interface Internals]] · [[SOLID Principles]] · [[Go Context & Cancellation]] · [[Go Sync Primitives]]