为什么我的Stringer接口方法没有被调用?当使用fmt.Println时。

huangapple go评论94阅读模式
英文:

Why isn't my Stringer interface method getting invoked? When using fmt.Println

问题

假设我有以下代码:

package main

import "fmt"

type Car struct{
	year int
	make string
}

func (c *Car)String() string{
	return fmt.Sprintf("{make:%s, year:%d}", c.make, c.year)
}

func main() {
	myCar := Car{year:1996, make:"Toyota"}
	fmt.Println(myCar)
}

当我调用fmt.Println(myCar)并且对象是一个指针时,我的String()方法会被正确调用。然而,如果对象是一个值,我的输出将使用Go内置的默认格式化,并且我的代码来格式化该对象不会被调用。

有趣的是,无论对象是基于指针还是基于值,如果我手动调用myCar.String(),它都能正常工作。

无论对象是基于值还是基于指针,我如何让对象以我想要的方式格式化在Println中使用?

我不想使用值方法来实现String,因为这意味着每次调用它时都会复制对象,这似乎不合理。而且我也不想总是手动调用.String(),因为我想让鸭子类型系统发挥作用。

英文:

Suppose I have the following code:

package main

import "fmt"

type Car struct{
	year int
	make string
}

func (c *Car)String() string{
	return fmt.Sprintf("{make:%s, year:%d}", c.make, c.year)
}

func main() {
	myCar := Car{year:1996, make:"Toyota"}
	fmt.Println(myCar)
}

When I call fmt.Println(myCar) and the object in question is a pointer, my String() method gets called properly. If, however the object is a value, my output is formatted using the default formatting built into Go and my code to format the said object is not called.

The interesting thing is in either case if I call myCar.String() manually it works properly whether my object is either a pointer or value.

How can I get my object formatted the way I want no matter if the object is value-based or pointer-based when used with Println?

I don't want to use a value method for String because then that means every time it's invoked the object is copied which seams unreasonable. And I don't want to have to always manually called .String() either because I'm trying to let the duck-typing system do it's work.

答案1

得分: 81

当调用fmt.Println时,myCar会被隐式转换为interface{}类型的值,正如你从函数签名中可以看到的那样。然后,fmt包中的代码会进行type switch来确定如何打印这个值,大致如下:

switch v := v.(type) {
case string:
    os.Stdout.WriteString(v)
case fmt.Stringer:
    os.Stdout.WriteString(v.String())
// ...
}

然而,fmt.Stringer的情况失败了,因为Car没有实现String(因为它是在*Car上定义的)。手动调用String是有效的,因为编译器看到String需要一个*Car,因此自动将myCar.String()转换为(&myCar).String()。对于任何涉及接口的事情,你必须手动处理。所以你要么在Car上实现String,要么总是传递一个指针给fmt.Println

fmt.Println(&myCar)
英文:

When calling fmt.Println, myCar is implicitly converted to a value of type interface{} as you can see from the function signature. The code from the fmt package then does a type switch to figure out how to print this value, looking something like this:

switch v := v.(type) {
case string:
    os.Stdout.WriteString(v)
case fmt.Stringer:
    os.Stdout.WriteString(v.String())
// ...
}

However, the fmt.Stringer case fails because Car doesn't implement String (as it is defined on *Car). Calling String manually works because the compiler sees that String needs a *Car and thus automatically converts myCar.String() to (&myCar).String(). For anything regarding interfaces, you have to do it manually. So you either have to implement String on Car or always pass a pointer to fmt.Println:

fmt.Println(&myCar)

答案2

得分: 27

方法

指针 vs. 值

关于接收器的指针 vs. 值的规则是,值方法可以在指针和值上调用,但指针方法只能在指针上调用。这是因为指针方法可以修改接收器;在值的副本上调用它们会导致这些修改被丢弃。

因此,为了使您的 String 方法在指针和值上都能正常工作,请使用值接收器。例如,

package main

import "fmt"

type Car struct {
    year int
    make string
}

func (c Car) String() string {
    return fmt.Sprintf("{make:%s, year:%d}", c.make, c.year)
}

func main() {
    myCar := Car{year: 1996, make: "Toyota"}
    fmt.Println(myCar)
    fmt.Println(&myCar)
}

输出:

{make:Toyota, year:1996}
{make:Toyota, year:1996}
英文:

> Methods
>
> Pointers vs. Values
>
> The rule about pointers vs. values for receivers is that value methods
> can be invoked on pointers and values, but pointer methods can only be
> invoked on pointers. This is because pointer methods can modify the
> receiver; invoking them on a copy of the value would cause those
> modifications to be discarded.

Therefore, for your String method to work when invoked on both pointers and values, use a value receiver. For example,

package main

import "fmt"

type Car struct {
	year int
	make string
}

func (c Car) String() string {
	return fmt.Sprintf("{make:%s, year:%d}", c.make, c.year)
}

func main() {
	myCar := Car{year: 1996, make: "Toyota"}
	fmt.Println(myCar)
	fmt.Println(&myCar)
}

Output:

{make:Toyota, year:1996}
{make:Toyota, year:1996}

答案3

得分: 7

package main

import "fmt"

type Car struct {
year int
make string
}

func (c *Car) String() string {
return fmt.Sprintf("{maker:%s, produced:%d}", c.make, c.year)
}

func main() {
myCar := Car{year: 1996, make: "Toyota"}
myOtherCar := &Car{year: 2013, make: "Honda"}
fmt.Println(&myCar)
fmt.Println(myOtherCar)
}

英文:

Define your fmt.Stringer on a pointer receiver:

package main

import "fmt"

type Car struct {
        year int
        make string
}

func (c *Car) String() string {
        return fmt.Sprintf("{maker:%s, produced:%d}", c.make, c.year)
}

func main() {
        myCar := Car{year: 1996, make: "Toyota"}
        myOtherCar := &Car{year: 2013, make: "Honda"}
        fmt.Println(&myCar)
        fmt.Println(myOtherCar)
}

Playground


Output:

{maker:Toyota, produced:1996}
{maker:Honda, produced:2013}    

Then, always pass a pointer to instances of Car to fmt.Println. This way a potentially expensive value copy is avoided under your control.

答案4

得分: 6

The OP further asked:
> OP: [当使用值接收器时] "这是否意味着如果我有一个大的结构体,那么每次它经过Println时都会被复制?"

以下实验证明了答案是"是的"(当使用值接收器时)。请注意,该实验中的String()方法会增加年份,并检查这如何影响打印输出。

type Car struct {
	year int
	make string
}

func (c Car) String() string {
	s := fmt.Sprintf("{ptr:%p, make:%s, year:%d}", c, c.make, c.year)
    // 增加年份以证明:c是副本还是引用?
	c.year += 1
	return s
}

func main() {
	myCar := Car{year: 1996, make: "Toyota"}
	fmt.Println(&myCar)
	fmt.Println(&myCar)
	fmt.Println(myCar)
	fmt.Println(myCar)
}

使用值接收器(c Car),以下打印输出显示Go会对Car结构体进行值复制,因为年份的增加在后续对Println的调用中没有反映出来:

<!-- language: lang-none -->

{ptr:%!p(main.Car={1996 Toyota}), make:Toyota, year:1996}
{ptr:%!p(main.Car={1996 Toyota}), make:Toyota, year:1996}
{ptr:%!p(main.Car={1996 Toyota}), make:Toyota, year:1996}
{ptr:%!p(main.Car={1996 Toyota}), make:Toyota, year:1996}

将接收器更改为指针(c *Car),但不更改其他内容,打印输出变为:

<!-- language: lang-none -->

{ptr:0xc420094020, make:Toyota, year:1996}
{ptr:0xc420094020, make:Toyota, year:1997}
{1998 Toyota}
{1998 Toyota}

即使在调用Println时提供指针作为参数,即fmt.Println(&amp;myCar),当使用值接收器时,Go仍然会对Car结构体进行值复制。OP希望避免进行值复制,我的结论是只有指针接收器才能满足这个要求。

英文:

The OP further asked:
> OP: [when a value receiver is used] "Does this basically mean that if I have a large struct, then every time it goes through Println it will be copied?"

The following experiment is evidence that the answer is "yes" (when a value receiver is used). Note that the String() method increments the year in this experiment, and check how this affects the printed output.

type Car struct {
	year int
	make string
}

func (c Car) String() string {
	s := fmt.Sprintf(&quot;{ptr:%p, make:%s, year:%d}&quot;, c, c.make, c.year)
    // increment the year to prove: is c a copy or a reference?
	c.year += 1
	return s
}

func main() {
	myCar := Car{year: 1996, make: &quot;Toyota&quot;}
	fmt.Println(&amp;myCar)
	fmt.Println(&amp;myCar)
	fmt.Println(myCar)
	fmt.Println(myCar)
}

With a value receiver (c Car), the following printed output shows that Go makes value copies of the Car struct, because the year increment is not reflected in subsequent calls to Println:

<!-- language: lang-none -->

{ptr:%!p(main.Car={1996 Toyota}), make:Toyota, year:1996}
{ptr:%!p(main.Car={1996 Toyota}), make:Toyota, year:1996}
{ptr:%!p(main.Car={1996 Toyota}), make:Toyota, year:1996}
{ptr:%!p(main.Car={1996 Toyota}), make:Toyota, year:1996}

Changing the receiver to a pointer (c *Car) but changing nothing else, the printed output becomes:

<!-- language: lang-none -->

{ptr:0xc420094020, make:Toyota, year:1996}
{ptr:0xc420094020, make:Toyota, year:1997}
{1998 Toyota}
{1998 Toyota}

Even when a pointer is provided as argument in a call to Println, i.e. fmt.Println(&amp;myCar), Go still makes a value copy of the Car struct when a value receiver is used. The OP wants to avoid value copies being made, and my conclusion is that only pointer receivers satisfy that requirement.

答案5

得分: 0

只与fmt的实现相关,而不是与Go相关。

由于spew以这种方式打印内容,因此会调用带有指针接收器的String()方法:


...

switch iface := v.Interface().(type) {
	case fmt.Stringer:
		defer catchPanic(w, v)
		if cs.ContinueOnMethod {
			w.Write(openParenBytes)
			w.Write([]byte(iface.String()))
			w.Write(closeParenBytes)
			w.Write(spaceBytes)
			return false
		}
		w.Write([]byte(iface.String()))
		return true
	}
英文:

It's only related to implementation of fmt instead of Go however.

String() with pointer receiver would be invoked by https://github.com/davecgh/go-spew since spew print things in this way:

v = reflect.ValueOf(arg)

...

switch iface := v.Interface().(type) {
	case fmt.Stringer:
		defer catchPanic(w, v)
		if cs.ContinueOnMethod {
			w.Write(openParenBytes)
			w.Write([]byte(iface.String()))
			w.Write(closeParenBytes)
			w.Write(spaceBytes)
			return false
		}
		w.Write([]byte(iface.String()))
		return true
	}

答案6

得分: -2

通常来说,最好避免通过静态初始化器给变量赋值,即

f := Foo{bar:1,baz:&quot;2&quot;}

这是因为如果你忘记通过&amp;foofoo作为指针传递,或者决定使用值接收器,就会出现你所说的问题,你最终会创建很多值的克隆。

相反,尽量默认将指针赋给静态初始化器,即

f := &amp;Foo{bar:1,baz:&quot;2&quot;}

这样,f将始终是一个指针,只有在你明确使用值接收器时才会得到一个值的副本。

(当然,有时你确实想要存储静态初始化器的值,但这些情况应该是边缘案例)

英文:

Generally speaking, it's best to avoid assigning values to variables via static initializers, i.e.

f := Foo{bar:1,baz:&quot;2&quot;}

This is because it can create exactly the complaint you're talking about, if you forget to pass foo as a pointer via &amp;foo or you decide to use value receivers you end up making a lot of clones of your values.

Instead, try to assign pointers to static initializers by default, i.e.

f := &amp;Foo{bar:1,baz:&quot;2&quot;}

This way f will always be a pointer and the only time you'll get a value copy is if you explicitly use value receivers.

(There are of course times when you want to store the value from a static initializer, but those should be edge cases)

huangapple
  • 本文由 发表于 2013年6月7日 12:59:17
  • 转载请务必保留本文链接:https://go.coder-hub.com/16976523.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定