Sam Chats
Sam Chats

Reputation: 2311

Weird Behavior of String() Method on Embedded Types in Go

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

Answers (1)

icza
icza

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 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.)

Upvotes: 7

Related Questions