Пользовательские ошибки в golang и указатели получателей - PullRequest
0 голосов
/ 14 мая 2018

Читая о получателях значений по сравнению с получателями указателей через Интернет и стекопотоком, я понимаю, что основное правило таково: если вы не планируете модифицировать получатель, а получатель относительно небольшой, указатели не нужны.

Затем, читая о реализации интерфейса error (например, https://blog.golang.org/error-handling-and-go),, я вижу, что во всех примерах функции Error() используется приемник указателя.

Пока мы неизменение получателя, и структура очень мала.

Мне кажется, что код без указателей намного приятнее (return &appError{} против return appError{}).

Есть ли причина, почему примерыиспользуете указатели?

Ответы [ 5 ]

0 голосов
/ 19 марта 2019

Обзор Go довольно хорошо объясняет общие причины для приемников указателей:

https://tour.golang.org/methods/8

Есть две причины использовать приемник указателей.

Во-первых, метод может изменять значение, на которое указывает его получатель.

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

0 голосов
/ 19 июля 2018

Мы можем смотреть на это с точки зрения обработки ошибок, а не создания ошибок.

Error Definiton Story Story

type ErrType1 struct {}

func (e *ErrType1) Error() string {
    return "ErrType1"
}

type ErrType2 struct {}

func (e ErrType2) Error() string {
    return "ErrType1"
}

Error Handler Side Story

err :=  someFunc()
switch err.(type) {
case *ErrType1
   ...
case ErrType2, *ErrType2
   ...
default
   ...
}

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

Для ErrType2, оба &ErrType2{}и ErrType2{} удовлетворяет интерфейсу.

Поскольку someFunc возвращает интерфейс error, вы никогда не узнаете, возвращает ли он значение структуры или указатель структуры, особенно когда вы не пишете someFunc.

Следовательно, использование приемника указателя не мешает пользователю вернуть указатель в качестве ошибки.

Как уже было сказано, все другие аспекты, такие как стек против кучи (распределение памяти), Давление ГХ) все еще применяется.

Выберите свою реализацию в соответствии с вашими вариантами использования.

В общем, я предпочитаю указатель-приемник по причине, которую я продемонстрировал выше.Я предпочитаю Friendly API, а не производительность, и иногда, когда тип ошибки содержит огромную информацию, он более производительный.

0 голосов
/ 14 мая 2018

Ошибки в go удовлетворяют только интерфейсу ошибок, то есть предоставляют метод .Error(). Создавая собственные ошибки или копаясь в исходном коде Go, вы обнаружите, что ошибки намного больше закулисны. Если структура заполняется в вашем приложении, чтобы избежать создания копий в памяти, более эффективно передавать ее как указатель. Кроме того, как показано в книге «Язык программирования Go»:

Функция fmt.Errorf форматирует сообщение об ошибке, используя fmt.Sprintf, и возвращает новое значение ошибки. Мы используем его для создания описательных ошибок путем последовательного добавления префикса дополнительной контекстной информации к исходному сообщению об ошибке. Когда ошибка в конечном итоге обрабатывается основной функцией программы, она должна обеспечить четкую причинную цепочку от основной проблемы до общего сбоя, напоминающего расследование аварии НАСА:

genesis: crashed: no parachute: G-switch failed: bad relay orientation

Поскольку сообщения об ошибках часто объединяются воедино, строки сообщений не должны начинаться с заглавной буквы, и следует избегать перевода строки. Получающиеся в результате ошибки могут быть длинными, но они будут самодостаточными, если найдены такими инструментами, как grep.

Из этого мы можем видеть, что если один «тип ошибки» содержит большое количество информации, и, в дополнение к этому, мы «объединяем» их вместе для создания подробного сообщения, использование указателей будет лучшим способом для достижения этой цели. .

0 голосов
/ 14 мая 2018

Во-первых, сообщение в блоге, на которое вы ссылались и взяли пример, appError - это , а не и error. Это обертка, которая переносчиков значения ошибки и другой связанной информации, используемой реализацией примеров, не раскрывается, и ни appError, ни *appError никогда не используются как error значение.

Так что приведенный вами пример не имеет никакого отношения к вашему актуальному вопросу. Но чтобы ответить на вопрос в заголовке:

В целом, последовательность может быть причиной. Если тип имеет много методов, а некоторым требуется получатель указателя (например, потому что они изменяют значение), часто бывает полезно объявить все методы с получателем указателя, поэтому не возникает путаницы относительно наборов методов типа и тип указателя.

Ответ относительно реализаций error: когда вы используете значение struct для реализации значения error, опасно использовать не указатель для реализации интерфейса error. Почему это так?

Потому что error - это интерфейс. И значения интерфейса сопоставимы . И они сравниваются путем сравнения значений, которые они переносят. И вы получите другой результат сравнения, основанный на том, какие значения / типы заключены в них! Потому что, если вы храните в них указатели, значения ошибок будут равны, если они хранят один и тот же указатель. И если вы храните в них не указатели (структуры), они равны, если значения структуры равны.

Чтобы уточнить это и показать пример:

Стандартная библиотека имеет пакет errors. Вы можете создавать значения ошибок из значений string, используя функцию errors.New(). Если вы посмотрите на его реализацию (errors/errors.go), все просто:

// Package errors implements functions to manipulate errors.
package errors

// New returns an error that formats as the given text.
func New(text string) error {
    return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

Реализация возвращает указатель на очень простое значение структуры. Это так, что если вы создадите 2 значения ошибки с одинаковым значением string, они не будут равны:

e1 := errors.New("hey")
e2 := errors.New("hey")
fmt.Println(e1, e2, e1 == e2)

Выход:

hey hey false

Это намеренно.

Теперь, если вы вернете не указатель:

func New(text string) error {
    return errorString{text}
}

type errorString struct {
    s string
}

func (e errorString) Error() string {
    return e.s
}

2 значения ошибок с одинаковыми string будут равны:

e1 = New("hey")
e2 = New("hey")
fmt.Println(e1, e2, e1 == e2)

Выход:

hey hey true

Попробуйте примеры на Go Playground .

Яркий пример, почему это важно: посмотрите на значение ошибки, хранящееся в переменной io.EOF:

var EOF = errors.New("EOF")

Ожидается, что реализации io.Reader вернут это конкретное значение ошибки в конец сигнала ввода. Таким образом, вы можете спокойно сравнить ошибку, возвращаемую Reader.Read() с io.EOF, чтобы определить, достигнут ли конец ввода. Вы можете быть уверены, что если они иногда возвращают пользовательские ошибки, они никогда не будут равны io.EOF, это то, что errors.New() гарантирует (потому что он возвращает указатель на неэкспортированное значение структуры).

0 голосов
/ 14 мая 2018

Нет :)

https://blog.golang.org/error-handling-and-go#TOC_2.

Интерфейсы Go позволяют обрабатывать все, что соответствует интерфейсу ошибок, с помощью кода, ожидающего error

type error interface {
    Error() string
}

Как вы упомянули, если вы не планируете изменять состояние, у вас мало стимулов для обхода указателей:

  • выделение для кучи
  • давление GC
  • изменяемое состояниеи параллелизм, и т. д.

На случайных разглагольствованиях, кстати, я лично считаю, что, видя подобные примеры, именно поэтому новые программисты go предпочитают приемники указателей по умолчанию.

...