5

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?

icza
  • 389,944
  • 63
  • 907
  • 827
Sam Chats
  • 2,271
  • 1
  • 12
  • 34
  • 1
    If you make the output of `TaxPayer.String` more distinctive (e.g. make it print "TaxPayer %d...."), you'll see the problem immediately. You'll see that `String` is invoked for *each embedded field* – Eli Bendersky May 13 '21 at 13:59

1 Answers1

7

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

icza
  • 389,944
  • 63
  • 907
  • 827
  • Thanks for the detailed explanation, but I still don't understand why `fmt.Println(engineer)` works while `fmt.Println(engineer.String())` raises the ambiguous selector error. I guess it's because of the way `fmt.Println()` works? Doesn't `fmt.Println()` attempt to always call the `String()` method of its argument? Is it because, as you mentioned, the default string representation doesn't involve the `String()` method call? – Sam Chats May 13 '21 at 14:39
  • @SamChats Please see the last section I just added. – icza May 13 '21 at 14:41
  • @SamChats Also I mentioned in the answer that the `fmt` package checks if a value being printed has a `String()` method. If it has, it will be called (for default formatting), if it doesn't, it can't be called. – icza May 13 '21 at 14:44
  • Thanks a lot, just went through it and I understand the nuances now. This behaviour had been bugging me for hours! – Sam Chats May 13 '21 at 14:45
  • So if I understand correctly, default formatting is invoked whenever String() method cannot be accessed (either it's not there or is ambiguous, like in the case of my example)? And the default formatting itself tries calling String() on each of the embedded and composed fields right? To construct the default format for printing? – Sam Chats May 13 '21 at 14:47
  • 1
    @SamChats Yes, something like that. The `fmt` package documents the rules that are used how a value is converted to `string`. The rules are also quoted in [this answer](https://stackoverflow.com/questions/30159980/why-does-error-have-priority-over-string/30160112#30160112). – icza May 13 '21 at 14:50