Reputation: 2311
I'm not able to understand how the String() method works for embedded structs in Go. Consider this:
type Engineer struct {
Person
TaxPayer
Specialization string
}
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("name: %s, age: %d", p.Name, p.Age)
}
type TaxPayer struct {
TaxBracket int
}
func (t TaxPayer) String() string {
return fmt.Sprintf("%d", t.TaxBracket)
}
func main() {
engineer := Engineer{
Person: Person{
Name: "John Doe",
Age: 35,
},
TaxPayer: TaxPayer{3},
Specialization: "Construction",
}
fmt.Println(engineer)
}
The output of this code is {name: John Doe, age: 35 3 Construction}
. But if I remove the Person.String()
method definition then the output is just 3
(it calls engineer.TaxPayer.String()
). However if I remove TaxPayer.String()
method definition as well, then the output is {{John Doe 35} {3} Construction}
. I initially thought there must be an implicit String()
method defined for the overall Engineer
struct, but there is no such method.
Why is method invocation behaving this way? If I instead have the methods for each embedded type named anything other than String()
(say Foo()
), and then try to do fmt.Println(engineer.Foo())
, I get an (expected) compilation error: ambiguous selector engineer.Foo
. Why is this error not raised when the methods' name is String()
instead?
Upvotes: 5
Views: 521
Reputation: 418505
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 structx
is called promoted ifx.f
is a legal selector that denotes that field or methodf
.[...] Given a struct type
S
and a defined typeT
, promoted methods are included in the method set of the struct as follows:
- If
S
contains an embedded fieldT
, the method sets ofS
and*S
both include promoted methods with receiverT
. The method set of*S
also includes promoted methods with receiver*T
.- If
S
contains an embedded field*T
, the method sets ofS
and*S
both include promoted methods with receiverT
or*T
.
The first sentence states "if x.f
is a legal selector". What does legal mean?
For a primary expression
x
that is not a package name, the selector expressionx.f
denotes the field or method
f
of the valuex
.[...] A selector
f
may denote a field or methodf
of a typeT
, or it may refer to a field or methodf
of a nested embedded field ofT
. The number of embedded fields traversed to reachf
is called its depth inT
. The depth of a field or methodf
declared inT
is zero. The depth of a field or method f declared in an embedded fieldA
inT
is the depth off
inA
plus one.The following rules apply to selectors:
- For a value
x
of typeT
or*T
whereT
is not a pointer or interface type,x.f
denotes the field or method at the shallowest depth inT
where there is such anf
. If there is not exactly onef
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.)
Upvotes: 7