Name Conflicts in Embedding

How Go resolves conflicting field and method names when multiple types are embedded into a single struct — covering shadowing rules, ambiguous selectors at the same depth, and how to write clear code that avoids confusion

When a struct embeds more than one type, it is possible for the same name to appear more than once — either as a field or as a method. Go’s promotion rules determine which one a bare reference resolves to, or whether the code will even compile.

A conflict is not necessarily an error. Some conflicts are resolved silently through shadowing. Others become compile-time errors that must be addressed before the program will build. Understanding the exact rules prevents surprises and helps you keep compositions easy to reason about.

What Causes Name Conflicts

Name conflicts arise from the simple fact that embedded fields and methods are promoted to the outer struct. Promotion means you can write c.Name instead of c.Embedded.Name. That shorthand is convenient, but it creates ambiguity when more than one embedded type contributes a member with the same identifier.

There are two distinct cases the compiler handles differently:

  • Different depths — the same name appears in an outer struct and in an embedded type (or in embedded types at different nesting levels).
  • Same depth — the same name appears in two or more embedded types at the same level, with no declaration in the outer struct itself.

Each scenario has its own resolution rules, and the programmer must understand both to write correct code.

How Go Resolves Promoted Names

When the compiler encounters an expression like x.Name, it searches for a field or method named Name using a well-defined sequence:

  1. First, it looks at the type of x itself — direct fields and methods of the struct.
  2. If nothing matches, it examines each embedded field, following a depth‑first, left‑to‑right order. For each embedded type, it recursively searches the fields and methods of that type, then its own embedded types, and so on.
  3. The first matching member found is selected. The search stops there.

Only one winner:

The first match wins, even if a later embedded type also contains a member with the same name. This is why depth and declaration order matter.

Conflicts at Different Depths: Shadowing

When a name appears at two different depths — once directly in the outer struct and once inside an embedded struct — the outer declaration shadows the inner one. The outer name is what you get when you write a bare reference. The inner name is still accessible, but only through an explicit path.

This is not a compiler error; it is an intentional design that allows the outer type to “override” what was promoted.

package main
import "fmt"
type Base struct {
    Label string
    Value int
}
type Wrapper struct {
    Base
    Label string // shadows Base.Label
}
func main() {
    w := Wrapper{
        Base:  Base{Label: "base-label", Value: 42},
        Label: "wrapper-label",
    }
    fmt.Println(w.Label)       // "wrapper-label"
    fmt.Println(w.Base.Label)  // "base-label"
    fmt.Println(w.Value)       // 42 (unambiguous, promoted)
}

The output is wrapper-label, base-label, and then 42. Without the explicit w.Base.Label, the shadowed field would be invisible to direct reference.

Shadowing hides, it does not delete:

A common misconception is that the inner field is overwritten or merged. It still exists as a separate field, and struct initialization must supply values for both if they differ. Forgetting to populate the shadowed field will leave it at its zero value without any compiler warning.

Conflicts at the Same Depth: Ambiguous Selectors

The real compile‑time error occurs when two embedded types at the same depth both provide a member with the same name, and the outer type has no declaration of its own for that name. In that situation, writing x.Name is ambiguous — the compiler cannot know which one you mean — so it refuses to compile.

package main
type A struct {
    ID    int
    Label string
}
type B struct {
    ID    int
    Count int
}
type Container struct {
    A
    B
}
func main() {
    c := Container{
        A: A{ID: 1, Label: "first"},
        B: B{ID: 2, Count: 100},
    }
    // fmt.Println(c.ID) // compiler error: ambiguous selector c.ID
    fmt.Println(c.A.ID, c.B.ID)
}

Uncommenting the line that refers to c.ID would trigger an error like:

ambiguous selector c.ID

The resolution is straightforward: qualify the reference with the embedded type name, as shown in the final line. Once qualified, both values are reachable and the code compiles.

Compile‑time block:

An ambiguous selector is a hard compilation failure. It does not matter that the two fields have the same type or that you can imagine the compiler “picking one.” The language specification explicitly forbids this, and the compiler will not allow it.

Methods Follow the Same Rules

The rules above apply to methods as well. If both A and B have a method called Print, calling c.Print() is ambiguous unless the outer type resolves the conflict (by declaring its own Print method, for example).

package main
import "fmt"
type A struct{}
func (A) Print() { fmt.Println("A") }
type B struct{}
func (B) Print() { fmt.Println("B") }
type Container struct {
    A
    B
}
func main() {
    c := Container{}
    // c.Print() // ambiguous selector c.Print
    c.A.Print()  // "A"
    c.B.Print()  // "B"
}

If you need Container to satisfy an interface that requires a Print method, you must define one explicitly:

func (c Container) Print() {
    c.A.Print()
}

Now c.Print() is unambiguous and works.

Interface Embedding and Duplicate Methods

Embedding interfaces can also create name conflicts, but the rules are slightly different. If you embed two interfaces that declare the same method, older Go releases (before Go 1.14) treated that as a duplicate method error, even when the signatures were identical. Since Go 1.14, duplicate methods with matching signatures are allowed — the resulting interface simply requires a single method of that name.

This is especially useful when combining small interfaces from different packages.

type Logger interface {
    Log(msg string)
}
type Tracer interface {
    Log(msg string)
    Trace() string
}
// Allowed in Go 1.14+ because both Log signatures match.
type Instrumented interface {
    Logger
    Tracer
}

Any concrete type that provides a Log(string) and a Trace() string method satisfies Instrumented.

Works in modern Go:

If you’re using Go 1.14 or later (which is almost certainly the case in any maintained project), identical method signatures across embedded interfaces will compile without issue. This eliminates a common source of friction when composing interface types.

Resolving Same‑Depth Conflicts Without Qualifying Every Call

Sometimes you cannot avoid having two embedded types that share a name, but you still want consumers of the outer struct to use a single, clear name. The simplest technique is to add your own field or method to the outer struct that “selects” the appropriate one.

type LoggerA struct{}
func (LoggerA) Log(msg string) { /* writes to stdout */ }
type LoggerB struct{}
func (LoggerB) Log(msg string) { /* writes to file */ }
type Service struct {
    LoggerA
    LoggerB
    active string
}
func (s Service) Log(msg string) {
    if s.active == "file" {
        s.LoggerB.Log(msg)
        return
    }
    s.LoggerA.Log(msg)
}

Now s.Log("hello") is unambiguous. The outer method serves as a dispatcher. The original embedded methods are still accessible if needed (s.LoggerA.Log(...)), but the promoted API remains clean.

Common Pitfalls

Assuming fields with the same name are merged. Two embedded types that both have Name do not produce a single Name field. They remain distinct fields belonging to separate embedded values. Struct initialisation must provide both values, and the ambiguous selector error prevents accidental misuse.

Forgetting that a shadowed field still needs initialisation. If Wrapper embeds Base and declares its own Label, you must supply a value for Base.Label when constructing the struct. Leaving it at its zero value may cause logic errors later. The compiler will not warn you.

Over‑relying on deep embedding chains. When embedding spans multiple levels (D embeds C embeds B embeds A), name resolution becomes difficult to mentally trace. A field that appears to be missing may simply be shadowed by a declaration further up the chain. Keeping embedding shallow makes the code easier to audit.

Ignoring declaration order. The left‑to‑right, depth‑first search means a member in an earlier‑declared embedded field can hide a member in a later one — even if the later one is at a shallower depth inside its own type. Read the struct definition carefully before changing the order.

Changing field order can break code:

Because promotion order depends on the sequence of field declarations, reordering embedded fields might silently change which member a bare reference resolves to. Tests that exercise promoted names are a good safety net.

Best Practices

  • Qualify when ambiguity is possible. Even when the compiler does not force it, c.A.Name is often clearer than a bare c.Name that happens to resolve to A.Name. The explicit form reduces the mental load for future readers.
  • Limit the number of embedded types. The more types you embed, the harder it becomes to predict name resolution. A struct that embeds three or four types each with dozens of fields is a maintenance headache.
  • Use interfaces to hide complexity. If the outer struct is going to expose a unified API, define a method on the outer type that delegates to the appropriate embedded member. This keeps the public surface clean and avoids ambiguous selectors.
  • Treat same‑depth conflicts as a design smell. If you find yourself hitting an ambiguous selector error, reconsider whether those two types really belong side‑by‑side, or whether one should be nested inside the other, or whether composition should be replaced by a named field.

Summary

Name conflicts in embedding are a direct consequence of Go’s promotion rules. The language resolves them with a simple priority system: the outermost declaration wins, and ties at the same depth are rejected. That rejection is not a weakness — it forces you to be explicit when the code genuinely is ambiguous.

The most important takeaway is that shadowing and ambiguous selectors are not bugs in Go; they are guards against accidental misuse. When you see an ambiguous selector error, the fix is always to qualify the reference — and that often improves readability. When you intentionally shadow a field, you are overriding a promoted member, and the original is still available through a full path.