skip to content

In Go, how do you make an embedded struct's method call the outer type's implementation instead of its own?

level: seniorimportance: should knowfreq 42%

answer

  1. give the base a hole to call into
  2. an interface field, not a base class
  3. the constructor installs what a vtable would
  4. an unwired field is a nil interface
  5. the value now points at itself

basics

~20 s

Give the embedded type an interface field for the step that varies, and assign the outer value to that field when you construct it. Dispatch then goes through the interface, because embedding alone provides none.

solid answer

~50 s

You build the indirection yourself. Declare an interface for the varying behaviour — say `Differ` with a `Diff` method — and make it a field of `BaseReconciler`, often an embedded interface field. `BaseReconciler.Reconcile` then calls `b.Diff(...)`, which goes through the interface value and lands on whatever concrete implementation was assigned. Because Go has no base-class constructor, nothing wires that field for you: the outer type's own constructor must do `n.Differ = n`, which is why this pattern is inseparable from a `New` function. The costs are real. Forget the assignment and the field is a nil interface, so the first call panics. Copy the wired struct and the copy's interface still points at the original. And the object now holds a reference to itself, which makes it easy to write a self-recursive call by accident. Before reaching for it, ask whether the outer type could simply own the entry point and delegate downward.

code

go · 26 lines
go
type Differ interface {
	Diff(desired, actual string) bool
}

type BaseReconciler struct {
	Differ // the hole the outer type fills
}

func (b BaseReconciler) Reconcile(desired, actual string) string {
	if b.Diff(desired, actual) {
		return "apply"
	}
	return "noop"
}

type NodeReconciler struct {
	BaseReconciler
}

func (n *NodeReconciler) Diff(desired, actual string) bool { return desired != actual }

func NewNodeReconciler() *NodeReconciler {
	n := &NodeReconciler{}
	n.Differ = n // no base constructor does this for you
	return n
}

go deeper

for a junior

Know the headline: Go gives you dispatch through interfaces only, so if a shared helper must call your version of a step, the helper has to hold an interface value you supply.

for a middle

Be able to build it: declare the interface, put it on the base as a field, and assign the outer value to it in a New function, explaining why no constructor does that automatically.

for a senior

Show that you have paid for this pattern in production. Name the nil-interface panic, the copy that keeps pointing at the original, and the self-recursion when the method is missing, and say when you would refuse the pattern and invert the call instead.

for a principal

Decide the house rule. Self-wiring types are only correct when built through their constructor, which constrains how the type can be exported and how teams may embed it, so weigh that against simply passing the varying behaviour in.

## The hole you have to cut yourself Embedding gives a struct the embedded type's methods, but the embedded type's code always runs against the embedded value and cannot reach the outer type. If you want the base's algorithm to call your step, you must give the base an explicit place to call into, and the only tool Go has for a call whose target is decided at run time is an interface value. So you declare the varying step as an interface, put it on the base as a field, and wire it at construction: ```go type Differ interface { Diff(desired, actual string) bool } type BaseReconciler struct { Differ } ``` Embedding the interface rather than naming the field is a common touch: it means `BaseReconciler` itself satisfies `Differ` (the method is promoted from the field), and code inside the base can write `b.Diff(...)` with no extra qualifier. A plain named field, `differ Differ`, works identically for dispatch and hides the method from the base's own method set — pick the one whose exported surface you actually want. ## Wiring, and the absence of a constructor Nothing runs when a struct is created, so the assignment is your job: ```go func NewNodeReconciler() *NodeReconciler { n := &NodeReconciler{} n.Differ = n return n } ``` That single line replaces what a class-based language does implicitly by installing a vtable pointer during construction. The consequence is that the type is only correct when built through its constructor. A zero value, a struct literal written by a caller in another package, or a value produced by decoding all skip the wiring. ## The three hazards, in the order they bite **A nil interface field.** The zero value of an interface is nil, and calling a method through a nil interface panics with an invalid memory address or nil pointer dereference — not at construction, but at the first call, possibly long after. If the base can work without the hook, guard it (`if b.Differ != nil`) or install a default implementation in the constructor. If it cannot, keep the field unexported so no caller can build a valid-looking value without going through your `New`. **Copying.** Once `n.Differ = n`, the value is self-referential. Copy the outer struct and you get a new value whose interface field still points at the original, so callbacks mutate the old object. Types wired this way should be used through a pointer and documented as non-copyable. **Accidental recursion.** If the outer type does not actually declare its own `Diff`, then `n.Differ = n` makes the base call the method promoted from the interface field — which is the field itself. That is an infinite recursion ending in a stack overflow. The wiring is only safe when the outer type really does declare the method at a shallower depth. ## Proving it works This is exactly the place for a test that asserts which implementation ran rather than what the result was. Wire the reconciler, have the outer `Diff` record that it was called, invoke the promoted entry point with a desired-versus-observed pair, and assert the record. Write the same test against the unwired zero value and watch it panic — that failing test is the documentation that this type must be constructed, not declared. ## When not to do it The pattern imports a class-based habit into a language that deliberately removed it, and it costs a constructor, a nil hazard, a copy hazard and a layer of indirection. Two alternatives usually read better in Go: - **Pass the behaviour, do not inherit it.** Make the varying step a plain field or a function value the base takes in its constructor. Same dispatch, no self-reference, no recursion trap, and the base has no opinion about who implements it. - **Invert the direction.** Let the outer type own the entry point and call the base for the parts it wants to reuse. The reader sees the sequence in the outer method instead of reconstructing it from a callback. Reserve the self-wiring form for the case where the base genuinely owns a fixed algorithm, several outer types vary one step of it, and the entry point must stay on the base. When you do use it, say so in the doc comment: the constructor is not optional.

  • What happens if a caller builds the struct with a literal and never assigns that interface field?
    The field is a nil interface and the first call through it panics with a nil pointer dereference, typically deep inside the base's method rather than at the point of construction. Keep the field unexported, install a default in the constructor, or nil-check before calling.
  • What breaks when someone copies the struct after it has been wired?
    The copy's interface field still points at the original value, so every callback runs against the old object's state while the copy's own fields are ignored. Use the type through a pointer and document it as non-copyable; copying is silent, so nothing warns you.
  • When would you avoid this callback pattern entirely?
    Whenever the outer type can own the entry point and call the base for the parts it reuses, or when the varying step can be passed in as a plain field or function value. Both avoid the self-reference, the nil hazard and the recursion trap, and they read as ordinary Go.
  • What goes wrong if the outer type does not actually declare the method the interface names?
    Assigning the outer value to the interface field makes the base call the method promoted from that same field, so it calls itself. The result is infinite recursion and a stack overflow at run time. The wiring is only valid when the outer type declares the method at a shallower depth.

saying these in an interview costs you the question

  • Expects embedding alone to route calls to the outer type
  • Reaches for reflect to find the containing struct at run time
  • Claims Go has abstract methods on an embedded struct
  • Exports the interface field and calls it without a nil check
  • Copies a self-wired value and expects the callback to follow