Почему errorString является структурой, а не строкой

Я читаю книгу программирования Go Go и в ней описывается пакет ошибок и интерфейс

package errors

type error interface {
    Error() string
}

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

type errorString struct { text string }

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

говорится

Основной тип errorString - это структура, а не строка, чтобы защитить свое представление от непреднамеренных (или преднамеренных) обновлений.

Что это значит? Разве пакет не скроет базовый тип, поскольку errorString не экспортируется?

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

package testerr

type Error interface {
        Error() string
}

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

type errorString string

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

И тестирование его с помощью предлагаемых кодов

func main() {
    err := errors.New("foo")
    err = "bar"
    fmt.Prinln(err)
}

В результате создается ошибка при компиляции

cannot use "bar" (type string) as type testerr.Error in assignment: string does not implement testerr.Error (missing Error method)

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

Ответ 1

Объяснение книги о "защите представления от непреднамеренных обновлений" выглядит для меня неправильным. Независимо от того, является ли errorString структурой или строкой, сообщение об ошибке по-прежнему является строкой, а строка неизменяема по спецификации.

Это не дискуссия о уникальности. Например, errors.New("EOF") == io.EOF оценивается как false, хотя обе ошибки имеют одно и то же основное сообщение. То же самое применимо, даже если errorString была строкой, если errors.New вернет указатель на нее (см. Мой пример.)

Вы могли бы сказать, что реализация struct error является идиоматической, поскольку так же, как стандартная библиотека вводит пользовательские ошибки. Взгляните на SyntaxError из пакета encoding/json:

type SyntaxError struct {
        Offset int64 // error occurred after reading Offset bytes
        // contains filtered or unexported fields
}

func (e *SyntaxError) Error() string { return e.msg }

(источник)

Кроме того, структура, реализующая интерфейс error, не имеет последствий для производительности и не потребляет больше памяти для реализации строки. См. Переместить структуры данных.

Ответ 2

Ваш пакет testerr работает очень хорошо, но он теряет основную особенность стандартного пакета ошибок на основе структуры: это неравноправие:

package main
import ( "fmt"; "testerr";  "errors" )

func main() {
    a := testerr.New("foo")
    b := testerr.New("foo")
    fmt.Println(a == b)  // true

    c := errors.New("foo")
    d := errors.New("foo")
    fmt.Println(c == d)  // false
}

С errorString, являющимся простой строкой, разные ошибки с тем же строковым содержимым становятся равными. В исходном коде используется указатель на struct, и каждый New выделяет новую структуру, поэтому разные значения, возвращаемые из New, различаются по сравнению с ==, хотя и равным текстом ошибки.

Никакой компилятор не может создавать тот же самый указатель здесь. И эта особенность "разных вызовов для новых продуктов с разными значениями ошибок" важна для предотвращения непреднамеренного равенства ошибок. Ваш тестер может быть изменен, чтобы получить это свойство, выполнив *errorString Error. Попробуйте: вам нужно временно взять адрес. Это "чувствует" неправильно. Можно представить себе фантастический компилятор, который внутренне выражает строковые значения и может возвращать один и тот же указатель (поскольку он указывает на ту же внутреннюю строку), которая нарушит это свойство хорошего неравенства.