skip to content

Your Adapter.Decode method gained its own type parameter and Adapter no longer satisfies your exported Decoder interface. How do you reshape the API?

level: seniorimportance: nice to knowfreq 18%

answer

  1. the error points at the use, not the method
  2. one method left the type's usable surface
  3. decide who names the type, and when
  4. parameter on the function, or on the interface
  5. consumers you cannot see also broke

basics

~20 s

Move the type parameter off the method. Put it on a package-level generic function that takes the adapter as an argument, or on the interface type itself, and leave the method with a concrete signature so it keeps satisfying the interface. A parametric method can never implement an interface method.

solid answer

~50 s

First name the cause: in Go a method that declares its own type parameter cannot implement an interface method, so parameterising `Decode` silently removed `Adapter` from every interface that required it — the compiler reports it at the assignment, not at the method. Then pick where the parameter really belongs. If consumers dispatch through `Decoder`, keep a method with a fixed signature and hoist the generic work into `func DecodeFrame[F any](a Adapter, frame []byte) (F, error)`; the method then calls it with a concrete type argument. If callers can commit to the target type when they obtain the value, parameterise the *interface* instead: `type Decoder[F any] interface { Decode([]byte) (F, error) }`. Only drop the interface entirely if nothing was really substituting for it. As a library author, do this before publishing: removing a method from an interface's reach is a breaking change for every consumer.

code

go · 13 lines
go
type Decoder interface {
	Decode(frame []byte) (any, error)
}

type Adapter struct{ topic string }

// Declares its own F, so it cannot implement Decoder.Decode.
func (a Adapter) Decode[F any](frame []byte) (F, error) {
	var out F
	return out, json.Unmarshal(frame, &out)
}

var d Decoder = Adapter{} // compile error: Adapter does not implement Decoder

go deeper

for a junior

Recognise the shape of the failure: the compiler complains where the value is assigned to the interface, and the cause is a method that now declares its own type parameter.

for a middle

Explain the two relocations available. The parameter can move onto a package-level function that takes the value, or onto the interface type itself, and each changes when the caller must name the type.

for a senior

Reason about blast radius. Show that your own tests will not catch this, that consumer-side interfaces you never see also break, and that adding a new generic function beside the method is cheaper than reshaping it.

for a principal

Treat a type parameter on an exported method as a permanent declaration that the operation is never reached through an interface, and make that call deliberately at the package boundary rather than discovering it from a consumer's build failure.

## The symptom You maintain an adapter package that translates another team's wire frames into your own structs. It exports an interface so consumers can substitute a fake in tests: ```go type Decoder interface { Decode(frame []byte) (any, error) } ``` Go 1.27 arrives and parametric methods look like an obvious improvement over returning `any`, so `Decode` gains a type parameter. Now the package no longer builds, and the error does not point at the method — it points at the assignment `var d Decoder = Adapter{}`, or at the call passing an `Adapter` to a function expecting a `Decoder`, saying the type does not implement the interface. That is the diagnostic to recognise: **the compiler rejects the use, not the declaration**, so a method that looks fine in isolation has quietly left the type's usable surface. The rule behind it: a generic method cannot implement an interface method. An interface value carries a table with one entry per method, and a method whose type argument is chosen per call has no single entry to name. ## Deciding where the parameter belongs The fix is not to un-generalise the code, it is to decide *who* chooses the type and *when*. ### Option 1 — package-level generic function, concrete method The safest reshaping when consumers already depend on the interface: ```go func DecodeFrame[F any](a Adapter, frame []byte) (F, error) { var out F return out, json.Unmarshal(frame, &out) } func (a Adapter) Decode(frame []byte) (any, error) { v, err := DecodeFrame[map[string]any](a, frame) return v, err } ``` Callers who know their target type call `DecodeFrame[Heartbeat](a, frame)` and get static typing. Callers who dispatch through `Decoder` keep working, unchanged. The interface is untouched, so nothing downstream breaks. The cost is a second entry point in the package's surface. ### Option 2 — parameterise the interface type If every consumer knows its target type at the moment it obtains the decoder, put the parameter one level up: ```go type Decoder[F any] interface { Decode(frame []byte) (F, error) } type Adapter[F any] struct{ topic string } func (a Adapter[F]) Decode(frame []byte) (F, error) { ... } ``` This is legal, and dispatch is ordinary because `Decoder[Heartbeat]` has a fixed signature. The tradeoff is where the commitment happens: the caller names `F` when constructing the adapter, and one adapter value can no longer serve two target types. If your adapter is per-topic and a topic has one frame type, that is the truthful model and the API gets *better*. If one adapter genuinely handles many frame types, this option is wrong. ### Option 3 — write into a caller-supplied destination ```go func (a Adapter) Decode(frame []byte, into any) error ``` This is what `json.Unmarshal` itself does. The signature is fixed, so the interface works, and the caller still names its type — at the cost of a run-time reflection step and losing the compile-time result type. Reach for it when the interface must stay one method wide and the reflection cost is already being paid downstream anyway. ### Option 4 — question the interface Sometimes the interface exists only because someone thought a package should export one. If there is exactly one implementation and no test double actually substitutes for it, deleting the interface and exporting the concrete `Adapter` with a parametric `Decode` is a legitimate answer. Say this out loud in an interview — the willingness to remove an abstraction rather than route around it is part of the judgment being tested. ## What a library author owes consumers The reason this question is worth asking is not the compile error, which is trivial once seen. It is the blast radius. A method that stops satisfying an interface is a source-breaking change for every consumer that assigned the type to that interface, wrote a wrapper around it, or embedded it. None of that shows up in your own package's tests if your package never assigns `Adapter` to `Decoder` itself. So before parameterising a method on an exported type: - Check whether the method appears in any interface your package exports or documents. - Check whether consumers plausibly declared their own consumer-side interface with that method — you cannot see those, and they will break. - Prefer adding a *new* generic function alongside the existing method over changing the method's shape. Additive change costs nothing; reshaping a method costs every consumer a migration. The general rule: a type parameter on a method is a strong statement that this operation will never be reached through an interface. That is a fine decision for an internal helper and an expensive one on an exported API you cannot take back.

  • Why did your own package's tests keep passing after the change?
    Because satisfaction is only checked where a value is assigned to the interface. If your package never assigns Adapter to Decoder, nothing in it fails; the break surfaces in consumer code, or at a compile-time assertion if you wrote one. Adding an assertion line for every exported type that is meant to satisfy an exported interface turns this into a local failure.
  • When is putting the parameter on the interface type the better answer?
    When the caller can commit to the target type at the moment it obtains the value rather than at each call. A per-topic adapter where a topic carries one frame type fits perfectly: Decoder[Heartbeat] has a fixed method signature, dispatch is ordinary, and the type shows up in the consumer's own declarations. It is wrong when one value must serve many target types.
  • Is adding a generic method to an exported type ever a safe, non-breaking change?
    Adding a brand-new method with its own type parameter is additive and safe, since nothing depended on it. What breaks is parameterising a method that already existed, because it leaves every interface that required it, including consumer-side interfaces you cannot see. Treat the second as a major-version change and prefer shipping a new package-level generic function instead.

saying these in an interview costs you the question

  • Tries to add the type parameter to the interface method
  • Switches to a pointer receiver hoping satisfaction returns
  • Says a type assertion at run time will recover the method
  • Treats it as a local compile error with no consumer impact
  • Deletes the generic entirely instead of relocating the parameter