If you embed types in a struct, the fields and methods of the embedded type get promoted to the embedder type. They "act" as if they were defined on the embedder type.
What does this mean? If type A
embeds type B
, and type B
has a method String()
, you can call String()
on type A
(the receiver will still be B
, this is not inheritance nor virtual method).
So far so good. But what if type A
embeds type B
and type C
, both having a String()
method? Then A.String()
would be ambiguous, therefore in this case the String()
method won't be promoted.
This explains what you experience. Printing Engineer
will have the default formatting (struct fields) because there would be 2 String()
methods, so none is used for Engineer
itself. Of course the default formatting involves printing the fields, and to produce the default string
representation of a value, the fmt
package checks if the value being printed implements fmt.Stringer
, and if so, its String()
method is called.
If you remove Person.String()
, then there is only a single String()
method promoted, from TaxPlayer
, so that is called by the fmt
package to produce the string
representation of the Engineer
value itself.
Same goes if you remove TaxPayer.String()
: then Person.String()
will be the only String()
method promoted, so that is used for an Engineer
value itself.
This is detailed in Spec: Struct types:
A field or method f
of an embedded field in a struct x
is called promoted if x.f
is a legal selector that denotes that field or method f
.
[...] Given a struct type S
and a defined type T
, promoted methods are included in the method set of the struct as follows:
- If
S
contains an embedded field T
, the method sets of S
and *S
both include promoted methods with receiver T
. The method set of *S
also includes promoted methods with receiver *T
.
- If
S
contains an embedded field *T
, the method sets of S
and *S
both include promoted methods with receiver T
or *T
.
The first sentence states "if x.f
is a legal selector". What does legal mean?
Spec: Selectors:
For a primary expression x
that is not a package name, the selector expression
x.f
denotes the field or method f
of the value x
.
[...] A selector f
may denote a field or method f
of a type T
, or it may refer to a field or method f
of a nested embedded field of T
. The number of embedded fields traversed to reach f
is called its depth in T
. The depth of a field or method f
declared in T
is zero. The depth of a field or method f declared in an embedded field A
in T
is the depth of f
in A
plus one.
The following rules apply to selectors:
- For a value
x
of type T
or *T
where T
is not a pointer or interface type, x.f
denotes the field or method at the shallowest depth in T
where there is such an f
. If there is not exactly one f
with shallowest depth, the selector expression is illegal.
- [...]
The essence is emphasized, and it explains why none of the String()
methods are called in the first place: Engineer.String()
could come from 2 "sources": Person.String
and TaxPayer.String
, therefore Engineer.String
is an illegal selector and thus none of the String()
methods will be part of the method set of Engineer
.
Using an illegal selector raises a compile time error (such as "ambiguous selector engineer.Foo"). So you get the error because you explicitly tried to refer to engineer.Foo
. But just embedding 2 types both having String()
, it's not a compile-time error. The embedding itself is not an error. The use of an illegal selector would be the error. If you'd write engineer.String()
, that would again raise a compile time error. But if you just pass engineer
for printing: fmt.Println(engineer)
, there is no illegal selector here, you don't refer to engineer.String()
. It's allowed. (Of course since method set of Engineer
does not have a promoted String()
method, it won't be called to produce string representation for an Engineer
–only when printing the fields.)