Подтвердить что ты не робот

Почему не вызывается мой метод интерфейса Stringer? При использовании 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(), потому что я пытаюсь, чтобы система типизации уток работала.

4b9b3361

Ответ 1

При вызове fmt.Println myCar неявно преобразуется в значение типа 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 нуждается в *Car и, таким образом, автоматически преобразует myCar.String() в (&myCar).String(). Для чего-либо, связанного с интерфейсами, вы должны сделать это вручную. Поэтому вам нужно либо реализовать String на Car, либо всегда передавать указатель на fmt.Println:

fmt.Println(&myCar)

Ответ 2

Методы

Указатели против значений

Правило о указателях и значениях для приемников - это методы значения могут быть вызваны указателями и значениями, но методы указателя могут быть только вызывается на указатели. Это связано с тем, что методы указателей могут получатель; вызывая их на копии значения, модификации, которые нужно отбросить.

Поэтому, чтобы ваш метод 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}

Ответ 3

Определите свой 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 to fmt.Println. Таким образом, под вашим контролем можно избежать потенциально дорогостоящей копии.

Ответ 4

ФП также спросил:

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, т.е. fmt.Println(&myCar), Go - прежнему делает стоимость копию Car структуры при использовании приемника значения. ОП хочет избежать создания копий значений, и я пришел к выводу, что только указатели получателей удовлетворяют этому требованию.

Ответ 5

Вообще говоря, лучше избегать назначения значений переменным через статические инициализаторы, т.е.

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

Это связано с тем, что он может создать именно жалобу, о которой вы говорите, если вы забыли передать foo в качестве указателя через &foo или, вы решили использовать получателей значений, делая много клонов ваших значений.

Вместо этого попробуйте назначить указатели на статические инициализаторы по умолчанию, т.е.

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

Таким образом, f всегда будет указателем, и единственный раз, когда вы получите копию значения, - если вы явно используете приемники значений.

(Конечно, есть время, когда вы хотите сохранить значение из статического инициализатора, но это должны быть случаи краев)