You add a second embedded type to a Go struct and importers' calls stop compiling - why?
answer
- two parents, one namespace
- the error is not where you changed things
- equal depth means neither one wins
- a method at depth zero settles it
basics
~20 sBoth embedded types declare the same method name at the same depth, so neither is promoted. The ambiguous selector is rejected where it is used, in the importer's code rather than yours. Fix it with a method on the outer type.
solid answer
~40 sEmbedding puts the embedded type's method and field names directly into the outer struct's namespace. Once a second embedded type declares the same name at the same depth, neither one is promoted: the selector is ambiguous, and the compiler rejects it *at the call site*. So my package still builds, while every importer that called the promoted method breaks. The name has also quietly left my type's method set, so it stops satisfying any interface that required it. The fix is a method declared on the outer type itself, which sits at depth zero and wins over both candidates, forwarding to the one I mean. The lesson is that promoted names are part of my compatibility surface, so an exported type should embed at most one thing.
code
go · 17 linestype SpecSource struct{}
func (SpecSource) Refresh() {}
type StatusSource struct{}
func (StatusSource) Refresh() {}
type Reconciler struct {
SpecSource
StatusSource
}
// r.Refresh() // compile error: ambiguous selector r.Refresh
// A method on the outer type sits at depth zero and wins.
func (r Reconciler) Refresh() { r.SpecSource.Refresh() }go deeper
Know that embedding two types that share a method name is a problem, and that the compiler reports it where the name is used rather than where the struct is declared.
Explain equal-depth ambiguity: neither candidate is promoted, the name leaves the outer type's method set, and a method declared on the outer type resolves it because it sits at depth zero.
Show that you can predict where the breakage lands - in importing packages, not yours - and that you would add the forwarding method and build known consumers before publishing the change.
Treat promoted names as part of the compatibility contract, and decide what your release review checks before an embedded field is added or an embedded dependency is bumped.
## Two embedded types share one namespace Consider a reconciler that compares desired state against observed state. It started life embedding one helper; a year later, with the original author gone, someone adds a second: ```go type SpecSource struct{} func (SpecSource) Refresh() {} type StatusSource struct{} func (StatusSource) Refresh() {} type Reconciler struct { SpecSource StatusSource } ``` This compiles. The struct declaration is perfectly legal - Go does not complain that two embedded types both offer `Refresh`. The trouble starts at the first use: ```go r.Refresh() // ambiguous selector r.Refresh ``` ### The rule underneath Promotion is resolved by **depth**. A selector on the outer type looks for the shallowest declaration of that name: a method or field declared directly on the outer type is depth zero, anything promoted from an embedded field is depth one, from a field embedded inside that, depth two, and so on. The shallowest wins outright. But **two candidates at equal depth are ambiguous**, and an ambiguous name is simply not promoted - it does not exist on the outer type at all. Three consequences follow, and each one bites differently. **1. The error appears where the name is used, not where it is declared.** Your own package may never call `r.Refresh()`, so `go build ./...` in your module is green. The breakage is in other people's packages, discovered after you have tagged and published the change. Everything reachable through the explicit path still works: `r.SpecSource.Refresh()` is unambiguous and always was. **2. The method leaves the method set, so interface satisfaction changes silently.** If `Reconciler` was being passed to something that accepts an interface with a `Refresh` method, that assignment now fails to compile - possibly in a completely different file from any call to `Refresh`. If the value was being stored in an interface variable at a distance, the error surfaces somewhere that looks unrelated to your change. **3. The same rule applies to fields.** Two embedded structs that both have a `Name` field make `r.Name` ambiguous in exactly the same way. Promotion is one mechanism for methods and fields alike. ### The fix Declare the method on the outer type. Depth zero beats both candidates, and you say explicitly which one you meant: ```go func (r Reconciler) Refresh() { r.SpecSource.Refresh() } ``` That is the whole repair, and it is worth noticing what it is: the forwarding method you would have written on day one if the field had simply been named. The ambiguity did not create the work, it deferred it - and deferred it past the point where the change is free. ### Diagnosing it in the wild The compiler message is direct - it names the ambiguous selector at the offending expression - so the diagnosis is fast once you see it. The hard part is that you may not be the one who sees it. Two habits catch it earlier: - Build the *consumers* you know about, not only your own module, before publishing a change that adds an embedded field. - Treat "added an embedded field" as an API change in review, the same way you would treat adding a method to an exported interface. Both alter what compiles for someone else. ### Why it happens at all The pull towards a second embed is real: you need one more collaborator's behaviour, the first one is already embedded, and matching the existing style feels like the polite thing to do. But embedding is not a style; it is a claim on a namespace shared between you and every type you embed. Two embedded types make it a namespace shared between three authors, none of whom coordinate - and either dependency can create the collision later just by adding a method in a minor release. The defensive posture is the same one that avoids most of these problems: embed at most one thing in an exported type, and only when its entire API is deliberately yours. Hold the rest in named fields and forward the two or three calls you actually need. The forwarding methods are boring, and boring is the property you want in a public surface.
- Does the ambiguity break your package or the importer's?Yours still compiles, because an ambiguous selector is only an error where it is written. The build breaks in every package that called the promoted method - typically after you have tagged and published. That asymmetry is exactly why promoted names belong in your compatibility surface, not in your implementation notes.
- Do ambiguous field names behave the same way as ambiguous methods?Yes. Promotion is one rule for both. Two embedded structs that each declare a `Name` field make `r.Name` a compile error, while `r.SpecSource.Name` keeps working. And a field or method declared directly on the outer type always wins over anything promoted.
- Can a dependency create this collision without you changing anything?Yes. If you embed two types you do not own, either team can add a method in a minor release that collides with the other's. Your code stops compiling for consumers on a version bump you did not review, which is the strongest practical argument against embedding more than one thing.
Two embedded types with the same method name are two people in the room who answer to the same name. The compiler will not guess which one you meant, and it only speaks up when someone actually calls the name.
saying these in an interview costs you the question
- Says declaration order decides which embedded method wins
- Expects the compile error at the struct declaration
- Thinks the ambiguous method still satisfies the interface
- Treats adding an embedded field as a non-breaking change
- Proposes renaming a method on a type another team owns