In Go, why does a promoted method keep calling the embedded type's method instead of the outer struct's redefinition?
answer
- no dispatch table behind a struct
- promotion is a forwarding wrapper
- the inner receiver is just a field
- the base cannot see what contains it
- no super, write the qualified call
basics
~20 sBecause embedding has no virtual dispatch. The promoted method runs with the embedded value as its receiver, and that receiver holds no link back to the outer struct, so calls inside it bind statically to the embedded type's own methods.
solid answer
~40 sPromotion is a forwarder, not an override table. When `NodeReconciler` embeds `BaseReconciler`, calling `n.Reconcile(...)` is exactly `n.BaseReconciler.Reconcile(...)`: the receiver inside the body is the embedded `BaseReconciler` value, which knows nothing about the `NodeReconciler` that contains it. So when `Reconcile` calls `b.Diff(...)`, the compiler resolves that against `BaseReconciler`'s method set at compile time and it always runs `BaseReconciler.Diff`, even though `NodeReconciler` declares a `Diff` of its own. That outer `Diff` is not an override — it is a different method on a different type, and only code that selects it on the outer value gets it. There is no `super` either: to reach the embedded version deliberately you write `n.BaseReconciler.Diff(...)`. If you need the base's code to call your implementation, you must give it an explicit hole — an interface field wired at construction.
code
go · 17 linestype BaseReconciler struct{ Name string }
func (b BaseReconciler) Diff(desired, actual string) bool { return desired != actual }
func (b BaseReconciler) Reconcile(desired, actual string) string {
if b.Diff(desired, actual) { // always BaseReconciler.Diff
return "apply"
}
return "noop"
}
type NodeReconciler struct {
BaseReconciler
}
// Meant as an override; the promoted Reconcile never reaches it.
func (n NodeReconciler) Diff(desired, actual string) bool { return false }go deeper
Remember the shape of the surprise: declaring a method with the same name as an embedded type's method does not replace it. Code inside the embedded type keeps calling its own version.
Explain the mechanism you are expected to know here: promotion generates a forwarder, the receiver is the embedded field, and the call inside it is resolved statically because Go keeps method tables only for interface values.
Demonstrate the production judgment: name the failure mode this produces in a long-running loop, show the test that asserts which implementation ran, and pick a fix, whether that is renaming, inverting the call, or adding an explicit interface hole.
Own the review rule rather than the individual bug. Decide whether your codebase permits shadowing an embedded method name at all, since every occurrence is a place where a future reader will assume dispatch that does not exist.
## What promotion compiles to When a struct embeds another type, the compiler makes the embedded type's methods part of the outer type's method set by generating forwarding wrappers. Conceptually, embedding `BaseReconciler` in `NodeReconciler` gives you: ```go func (n NodeReconciler) Reconcile(desired, actual string) string { return n.BaseReconciler.Reconcile(desired, actual) } ``` That is the whole mechanism. The receiver inside `Reconcile`'s body has type `BaseReconciler`. It is a field of the outer value (or a copy of one, for a value receiver), and it contains no pointer, tag, or type descriptor identifying the struct that embeds it. ## Why the inner call cannot see your method Inside `BaseReconciler.Reconcile`, a call like `b.Diff(desired, actual)` is resolved by the compiler against the static type of `b`, which is `BaseReconciler`. There is exactly one candidate: `BaseReconciler.Diff`. Nothing is looked up at run time, because Go puts no dispatch table behind a struct. Method tables exist in Go only for interface values, and only for the methods the interface declares. So the `Diff` you declared on `NodeReconciler` is not an override in any sense the language recognises. It is a separate method on a separate type that happens to share a name. Call sites that write `n.Diff(...)` on the outer value get yours; every call that originates inside `BaseReconciler`'s own code gets the base's. In a class-based language both would land on the subclass method through the vtable; in Go the two worlds never meet. Embedding a pointer (`*BaseReconciler`) changes nothing here. The receiver is still a `*BaseReconciler`, and a pointer to the base still carries no reference to whatever contains it. ## The failure this produces in real code A controller has a `BaseReconciler` whose `Reconcile` compares a desired-state record with the observed one via `Diff`, and applies a change when they differ. A team adds `NodeReconciler`, embeds the base, and declares its own `Diff` that treats a missing optional field as equal. Everything compiles. The tests that call `n.Diff` directly pass. In production the controller keeps reapplying changes, because `Reconcile` — the entry point the control loop actually calls — is still running `BaseReconciler.Diff`. Whoever writes the postmortem has to explain that the code contained an override that was never invoked, and that the compiler had nothing to warn about. ## How to catch it The cheap check is a unit test that asserts which implementation ran, not just what came back. Give the outer `Diff` an observable side effect — increment a counter on the receiver, or record the call — then invoke the promoted `Reconcile` and assert the counter moved. It will not. A test written that way turns an invisible semantic gap into a failing assertion in seconds, and it is worth writing every time someone shadows an embedded method name. ## What to do instead Three honest options: 1. **Do not redefine the name.** If the outer type needs different behaviour, give it a differently named method and let callers choose. Shadowing an embedded method name and expecting dispatch is the trap. 2. **Invert the call.** Let the outer type own the entry point and call into the base explicitly: `func (n NodeReconciler) Reconcile(...)` that does its own diffing and delegates the parts it wants to `n.BaseReconciler`. 3. **Give the base an explicit hole.** Declare an interface for the varying step and put it on the base as a field, wired at construction to the outer value. That restores dispatch, but only because you built the indirection by hand. ## Reaching the embedded version on purpose Go has no `super`. When the outer method wants the base's behaviour plus something extra, you write the qualified call yourself: `n.BaseReconciler.Diff(desired, actual)`. That explicitness is the point — the call target is visible in the source rather than decided by a hierarchy the reader has to reconstruct.
- Does embedding a pointer to the base instead of a value make the callback work?No. The receiver becomes a *BaseReconciler rather than a BaseReconciler, but it still carries no reference to the struct that embeds it. Whether the embedded field is a value or a pointer changes copying and mutation semantics, never dispatch.
- Does the outer type's method ever run at all, then?Yes — any call that selects it on the outer value runs it, so n.Diff(...) is your implementation. What never happens is the base's own code reaching it; every call originating inside the embedded type's methods stays inside the embedded type.
- How would you prove in a test which implementation actually ran?Assert on a side effect rather than a return value: have the outer method increment a counter or append to a slice on the receiver, call the promoted entry point, and assert the counter moved. Comparing return values alone hides the gap whenever both implementations happen to agree.
- How do you call the embedded type's version from a method of the same name on the outer type?Write the qualified selector: n.BaseReconciler.Diff(desired, actual). Go has no super, so the call target is spelled out in the source. Note this is a normal call on the embedded field, not a special form the language treats differently.
saying these in an interview costs you the question
- Calls the outer method an override, expecting virtual dispatch
- Looks for a super call back into the embedded method
- Thinks embedding a pointer to the base enables the callback
- Believes the embedded value knows its containing struct
- Assumes shadowing a method name changes what the base calls