57

假设我有以下代码:

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 使用 value 方法,因为这意味着每次调用它时都会复制对象,这接缝是不合理的。而且我不想总是手动调用 .String() ,因为我试图让鸭子打字系统完成它的工作。

4

6 回答 6

76

从函数签名中可以看出,调用时fmt.PrintlnmyCar会隐式转换为类型的值。interface{}包中的代码fmt然后进行类型切换以确定如何打印此值,如下所示:

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需要 a*Car并因此自动转换myCar.String()(&myCar).String(). 对于有关接口的任何事情,您都必须手动完成。因此,您要么必须实现StringonCar要么总是将指针传递给fmt.Println

fmt.Println(&myCar)
于 2013-06-07T07:32:24.917 回答
27

方法

指针与值

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

因此,为了让您的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}
于 2013-06-07T07:52:58.117 回答
7

在指针接收器上定义 fmt.Stringer:

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

操场


输出:

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

然后,总是将指向 Car 实例的指针传递给 fmt.Println。这样,在您的控制下避免了潜在的昂贵价值副本。

于 2013-06-08T10:20:42.603 回答
6

OP进一步问道:

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)
    // increment the year to prove: is c a copy or a reference?
    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

{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)但不更改任何其他内容,打印输出变为:

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

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

于 2018-08-14T22:16:08.490 回答
0

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

带有指针接收器的 String() 将由https://github.com/davecgh/go-spew调用,因为spew以这种方式打印内容:

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
    }
于 2019-11-19T15:12:46.070 回答
-2

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

f := Foo{bar:1,baz:"2"}

这是因为它可以准确地创建您正在谈论的投诉,如果您忘记foo作为指针传递&foo 或者您决定使用值接收器,您最终会复制大量值。

相反,尝试默认分配指向静态初始化器的指针,即

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

这种方式f将始终是一个指针,并且您获得值副本的唯一时间是您明确使用值接收器。

(当然有时您想要存储来自静态初始化程序的值,但那些应该是边缘情况)

于 2014-12-26T12:16:52.147 回答