{"slug": "method-sets-embedding-and-interface-satisfaction-in-go-the-hidden-contract-api", "title": "Method Sets, Embedding, and Interface Satisfaction in Go: The Hidden Contract Behind API Boundaries", "summary": "A developer detailed how Go's method set rules for embedded types create subtle interface-satisfaction failures in large backend systems, particularly in AI integration adapters wrapping LLM providers. The writeup shows that embedding a type by value rather than by pointer silently omits pointer-receiver methods, so a concrete type fails to satisfy an interface across package boundaries, and warns that the pattern often survives code review undocumented.", "body_md": "Go's interface satisfaction is structural and compile-time, which sounds safe until you're debugging why a concrete type that clearly has all the right methods refuses to satisfy an interface in a different package—or worse, satisfies it silently and then behaves incorrectly at runtime because pointer receivers were embedded into a value type that gets copied across a serialization boundary.\n\nIn large backend systems with multiple service layers, SDK packages, and AI integration adapters, the method set rules aren't a language curiosity. They're a load-bearing part of your API contract, and the failure modes are subtle enough to survive code review.\n\nGo specifies method sets precisely. For a type `T`:\n\n`T` contains all methods with receiver `*T` contains all methods with receiver For embedded types:\n\n`S` contains an embedded field `*S` includes the promoted methods of both The asymmetry in that last rule is where backends get burned.\n\n```\ntype Store interface {\n    Get(ctx context.Context, key string) ([]byte, error)\n    Put(ctx context.Context, key string, val []byte) error\n    Close() error\n}\n\ntype RedisStore struct {\n    client *redis.Client\n}\n\n// Only pointer receiver methods exist\nfunc (r *RedisStore) Get(ctx context.Context, key string) ([]byte, error) { ... }\nfunc (r *RedisStore) Put(ctx context.Context, key string, val []byte) error { ... }\nfunc (r *RedisStore) Close() error { ... }\n\ntype CachingLayer struct {\n    RedisStore       // embedded by value, NOT pointer\n    local *sync.Map\n}\n```\n\n`CachingLayer` embeds `RedisStore` by value. Its method set includes only the methods of `RedisStore` (receiver `RedisStore`), which is empty. `*CachingLayer` gets the promoted methods of `*RedisStore`. So `*CachingLayer` satisfies `Store`, but `CachingLayer` does not.\n\nThe compiler catches the direct assignment. But it doesn't catch this pattern, which appears in real service wiring:\n\n```\nfunc NewCachingLayer(r RedisStore) Store {\n    c := CachingLayer{RedisStore: r}\n    return c  // compile error: CachingLayer does not implement Store\n}\n```\n\nChange the return to `return &c` and it works. The failure mode is that many teams embed types by value when they should embed by pointer, learn the pattern by trial and error, but never document why—leaving the next engineer to rediscover it when they add a new method with a pointer receiver six months later.\n\nWithin a single package, your editor and the compiler give immediate feedback. The problem metastasizes when the concrete type lives in an internal package, the interface lives in a public SDK package, and the wiring lives in a third service layer.\n\nConsider a backend AI integration where you're wrapping an LLM provider behind a retrieval interface:\n\n```\n// sdk/retrieval/interface.go\npackage retrieval\n\ntype Retriever interface {\n    Query(ctx context.Context, q Query) (Results, error)\n    Embed(ctx context.Context, text string) ([]float32, error)\n    Health(ctx context.Context) error\n}\n\n// internal/openai/adapter.go\npackage openai\n\ntype Adapter struct {\n    cfg    Config\n    client *http.Client\n    mu     sync.Mutex\n    cache  map[string][]float32\n}\n\nfunc (a *Adapter) Query(ctx context.Context, q retrieval.Query) (retrieval.Results, error) { ... }\nfunc (a *Adapter) Embed(ctx context.Context, text string) ([]float32, error) { ... }\nfunc (a *Adapter) Health(ctx context.Context) error { ... }\n\n// service/wiring.go\npackage service\n\nfunc wire(cfg Config) retrieval.Retriever {\n    adapter := openai.Adapter{cfg: cfg} // value, not pointer\n    return adapter                       // compile error\n}\n```\n\nThe error is caught. But consider a test double that embeds the real adapter:\n\n```\ntype InstrumentedAdapter struct {\n    openai.Adapter   // value embed\n    metrics *Metrics\n}\n\nfunc (i *InstrumentedAdapter) Query(ctx context.Context, q retrieval.Query) (retrieval.Results, error) {\n    defer i.metrics.Record(\"query\", time.Now())\n    return i.Adapter.Query(ctx, q)  // method promoted from *Adapter\n}\n```\n\n`*InstrumentedAdapter` satisfies `retrieval.Retriever` because `*InstrumentedAdapter`'s method set includes the promoted methods of `*openai.Adapter`. But `InstrumentedAdapter.Adapter` is a copy. Any mutation inside `Adapter`—updating the cache, rotating credentials, draining a connection—operates on the copy. The original is untouched. In a long-running service with connection reuse or token refresh, this is a latent correctness bug that manifests under load, not in unit tests.\n\nThe copy problem compounds when types cross serialization boundaries. In event-driven microservices, it's common to pass handler structs through configuration loading or dependency injection frameworks that use reflection.\n\nReflection in Go uses `reflect.Value`. When you call `reflect.ValueOf(adapter)` on a value type, you get an unaddressable value. Methods with pointer receivers are not in the method set of the value, so they're not callable through reflection on that value. The DI framework silently falls back, skips the method, or panics.\n\nThe invariant that matters operationally: **if any method on a type has a pointer receiver, the type should only ever be passed and stored as a pointer.** This should be enforced at the package boundary with a constructor:\n\n```\n// Force pointer-only usage\nfunc NewAdapter(cfg Config) *Adapter {\n    return &Adapter{\n        cfg:    cfg,\n        client: &http.Client{Timeout: cfg.Timeout},\n        cache:  make(map[string][]float32),\n    }\n}\n\n// Prevent value copies with a noCopy guard for go vet\ntype Adapter struct {\n    noCopy noCopy\n    cfg    Config\n    client *http.Client\n    mu     sync.Mutex\n    cache  map[string][]float32\n}\n\ntype noCopy struct{}\nfunc (*noCopy) Lock()   {}\nfunc (*noCopy) Unlock() {}\n```\n\n`go vet`'s `copylocks` analysis will flag any copy of `Adapter` because `sync.Mutex` is itself guarded. Adding `noCopy` makes the intent explicit and catches cases where the mutex is extracted before embedding.\n\nInterface width—number of methods—has a direct relationship to testability and coupling across package boundaries. A `Store` interface with `Get`, `Put`, `Delete`, `List`, `Watch`, `Compact`, and `Health` methods forces every test double to implement all eight methods, most of which are irrelevant to the unit under test.\n\nThe production consequence is that teams create `BaseStore` structs with no-op implementations, which embed well but obscure which methods actually matter for a given code path. When a new method is added to the interface (say, `Compact`), every `BaseStore` silently satisfies the new interface with a no-op, hiding the fact that the caller now has a latent correctness issue.\n\nNarrow interfaces, defined by the consumer not the producer, solve this:\n\n```\n// Consumer-side interface in the background job package\npackage compactor\n\ntype Compactable interface {\n    Compact(ctx context.Context, before time.Time) (int64, error)\n    Health(ctx context.Context) error\n}\n```\n\nThe Redis store and the MongoDB store each implement `Compactable` if they implement those two methods—regardless of what else they implement. No embedding of no-ops. No fake base structs. Test doubles need two methods. The compiler enforces that the real type satisfies the consumer's contract.\n\nGo generics add a third axis: type constraints. A constraint is an interface, and method sets apply to type parameters. But there's a subtlety: you cannot use a method with a pointer receiver on a type parameter constrained to a non-pointer type:\n\n```\ntype Initializable interface {\n    Init() error\n}\n\nfunc Setup[T Initializable](v T) error {\n    return v.Init()\n}\n\ntype Worker struct{ name string }\nfunc (w *Worker) Init() error { return nil }\n\n// Setup(Worker{}) fails: Worker does not implement Initializable\n// Setup(&Worker{}) works: *Worker implements Initializable\n```\n\nIn generic infrastructure code—connection pool factories, middleware chains, SDK client builders—this forces a design choice: constrain on `*T` explicitly, or require the caller to pass pointers. The former requires a two-constraint pattern:\n\n```\nfunc Setup[T any, PT interface {\n    *T\n    Initializable\n}](factory func() T) error {\n    v := factory()\n    pt := PT(&v)\n    return pt.Init()\n}\n```\n\nThis is correct but adds indirection that callers must understand. For most backend service code, the simpler answer is: define your generic constraints over pointer types from the start, and document why.\n\nWhen designing types at a package boundary in a Go backend service:\n\n**1. If any method needs to mutate state or holds a lock, use a pointer receiver everywhere on that type.** Mixed receivers on a single type create a fragmented method set that satisfies some interfaces but not others depending on whether you have a pointer.\n\n**2. Embed by pointer when the embedded type has pointer-receiver methods and shared mutable state must propagate.** Embed by value only for pure value types with no pointer receivers and no mutation.\n\n**3. Define interfaces in the consumer package, not the producer package.** Width should reflect what the consumer actually calls. This makes test doubles cheap and prevents interface creep.\n\n**4. Use constructors that return pointers for any type with a mutex, channel, or pointer-receiver method.** Add a `noCopy` guard. Make value copying a `go vet` failure, not a code review catch.\n\n**5. For generic infrastructure, decide up front whether type parameters are pointer-constrained.** Mixed generics that work on both pointer and value types require the two-constraint pattern and cost readability. If your concrete types are all structs with pointer receivers—and in backend services they usually are—constrain on pointer types and document it.\n\n**6. Audit interface satisfaction across package boundaries as part of CI.** A compile-time assignment check in a `_test.go` or `doc.go` file makes satisfaction explicit and breaks the build if a refactor removes a method:\n\n``` js\nvar _ retrieval.Retriever = (*openai.Adapter)(nil)\nvar _ retrieval.Retriever = (*InstrumentedAdapter)(nil)\n```\n\nThis is one line per type. It costs nothing at runtime. It makes the contract visible and breaks the build the moment a method disappears.\n\nMethod sets are not a beginner topic. They're the mechanical foundation of every interface-based seam in a Go backend, and getting them wrong quietly—through value embedding, receiver inconsistency, or interface width creep—produces bugs that survive testing and surface under the conditions that are hardest to reproduce: long-running mutations, high concurrency, and cross-package reflection.", "url": "https://wpnews.pro/news/method-sets-embedding-and-interface-satisfaction-in-go-the-hidden-contract-api", "canonical_source": "https://dev.to/neeraj_singhi_golang/method-sets-embedding-and-interface-satisfaction-in-go-the-hidden-contract-behind-api-boundaries-3m83", "published_at": "2026-09-24 10:45:01+00:00", "updated_at": "2026-09-24 11:01:27.470546+00:00", "lang": "en", "topics": ["developer-tools", "ai-infrastructure"], "entities": ["Go", "Redis"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/method-sets-embedding-and-interface-satisfaction-in-go-the-hidden-contract-api", "markdown": "https://wpnews.pro/news/method-sets-embedding-and-interface-satisfaction-in-go-the-hidden-contract-api.md", "text": "https://wpnews.pro/news/method-sets-embedding-and-interface-satisfaction-in-go-the-hidden-contract-api.txt", "jsonld": "https://wpnews.pro/news/method-sets-embedding-and-interface-satisfaction-in-go-the-hidden-contract-api.jsonld"}}