Go 中 String() 方法在嵌入式类型上的奇怪行为

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

我无法理解 String() 方法如何用于 Go 中的嵌入式结构。考虑一下:

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

这段代码的输出是{name: John Doe, age: 35 3 Construction}。但是,如果我删除 Person.String() 方法定义,则输出只是 3(它调用 engineer.TaxPayer.String())。但是,如果我也删除 TaxPayer.String() 方法定义,则输出为 {{John Doe 35} {3} Construction}。我最初认为必须为整个 Engineer 结构定义一个隐式 String() 方法,但没有这样的方法。

为什么方法调用会这样?如果我改为将每个嵌入类型的方法命名为 String() 以外的任何名称(例如 Foo()),然后尝试执行 fmt.Println(engineer.Foo()),我会得到一个(预期的)编译错误:ambiguous selector engineer.Foo。为什么当方法名称为 String() 时不会引发此错误?

如果在结构中嵌入类型,嵌入类型的字段和方法将提升为嵌入类型。它们的“行为”就好像它们是在嵌入器类型上定义的一样。

这是什么意思?如果类型 A 嵌入了类型 B,并且类型 B 有一个方法 String(),您可以在类型 A 上调用 String()(接收者将仍然是B,这不是继承也不是虚方法)。

到目前为止一切顺利。但是如果类型 A 嵌入类型 B 和类型 C,两者都有一个 String() 方法呢?那么 A.String() 将是不明确的,因此在这种情况下 String() 方法将不会被提升。

这解释了你的经历。打印 Engineer 将具有默认格式(结构字段),因为会有 2 个 String() 方法,因此 none 用于 Engineer 本身 .当然,默认格式涉及打印字段,并且要生成值的 default string 表示,fmt 包检查打印的值是否实现 fmt.Stringer,如果是,则调用其 String() 方法。

如果您删除 Person.String(),那么只有一个 String() 方法从 TaxPlayer 中提升,因此 fmt 包会调用它来生成string Engineer 值本身的表示。

如果您删除 TaxPayer.String() 也是如此:那么 Person.String() 将是唯一提升的 String() 方法,因此它用于 Engineer 值本身。

这在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.

第一句陈述“如果x.f是一个合法的选择器”合法是什么意思?

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.
  • [...]

强调了本质,它解释了为什么首先调用 String() 方法中的 none:Engineer.String() 可能来自 2 个“来源”:Person.StringTaxPayer.String,因此 Engineer.String 是非法选择器,因此 String() 方法中的 none 将成为 Engineer 方法集的一部分。

使用非法选择器会引发编译时错误(例如 "ambiguous selector engineer.Foo")。所以你得到错误是因为你 明确地 试图引用 engineer.Foo。但是仅仅嵌入两种类型都具有 String(),这不是编译时错误。嵌入本身不是错误。使用非法选择器将是错误的。如果你写 engineer.String(),那会再次引发编译时错误。但是如果你只是通过engineer来打印:fmt.Println(engineer),这里没有非法选择器,你不引用engineer.String()。这是允许的。 (当然,由于 Engineer 的方法集没有提升的 String() 方法,因此不会调用它来为 Engineer 生成字符串表示形式——仅在打印字段时。)