You embed two types into the same struct, and both happen to have a field with the same name at the same depth. Nothing about the embedding declarations themselves complains. It's only the moment something actually tries to read the promoted field through the outer struct that the compiler stops you — because it genuinely has no way to know which one you meant.
type Base struct {
ID int
}
type Audit struct {
ID int
}
type Record struct {
Base // embeds Base.ID
Audit // embeds Audit.ID -- also named ID, same depth
}
r := Record{Base: Base{ID: 1}, Audit: Audit{ID: 2}}
fmt.Println(r.ID) // ambiguous: which ID?
// ./embed_bug.go:20:16: ambiguous selector r.IDEmbedding declares a field using only a type name, with no field name of its own. The embedded type’s fields and methods are then “promoted” to the outer struct, so they can be accessed as if they belonged to it directly. This is Go’s deliberate substitute for classical inheritance: it’s plain composition under the hood (the outer struct literally contains the inner one as a field), not a subtype relationship — an embedded type’s methods don’t know about the struct embedding them, and there’s no polymorphism via embedding alone.
Field and method promotion
An embedded type's fields and methods become accessible directly on the outer type, without an extra level of dot notation:
type Base struct {
Name string
}
func (b Base) Describe() string {
return "I am " + b.Name
}
type Employee struct {
Base // embedded: no field name, just the type
Salary int
}
e := Employee{Base: Base{Name: "Alice"}, Salary: 50000}
fmt.Println(e.Name) // promoted field, no e.Base.Name needed
fmt.Println(e.Describe()) // promoted methodIt's composition, not inheritance
The distinction matters in practice: if Describe were changed to call another method that Employee overrides, the embedded Base.Describe would still call Base's version, not Employee's — there's no dynamic dispatch the way a real subclass would have. You can still access the embedded value explicitly via its type name (e.Base), and if the outer type declares its own field or method with the same name, that shadows the promoted one. That shadowing rule is exactly what rescues the callout above: naming the field at the outer level resolves an otherwise-ambiguous promotion. Where neither embedded type is shadowed by an outer field — both ambiguous names sit at the same depth, with nothing to break the tie — the explicit path through each type's own name is the only way to reach either one:
r := Record{Base: Base{ID: 1}, Audit: Audit{ID: 2}}
fmt.Println(r.Base.ID, r.Audit.ID) // spell out the path -- no ambiguity
// 1 2Interface embedding
Interfaces can embed other interfaces too, composing a larger interface from smaller ones — this is how the standard library builds up io.ReadWriter from io.Reader and io.Writer without repeating method signatures.
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface { // composed from both
Reader
Writer
}