[Перевод] Освобождаемся от обработки ошибок, устраняя ошибки

-twgqf5sqhxuyrhnilflk-luxtc.jpeg

Go2 имеет целью уменьшить накладные расходы на обработку ошибок, но знаете ли вы, что лучше, чем улучшенный синтаксис для обработки ошибок?

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

Эта статья черпает вдохновение из главы «Define Errors Out of Existence» книги » A philosophy of Software Design» Джона Оустерхаута. Я постараюсь применить его совет к Go.

Пример первый


Вот функция для подсчета количества строк в файле:

func CountLines(r io.Reader) (int, error) {
        var (
                br    = bufio.NewReader(r)
                lines int
                err   error
        )

        for {
                _, err = br.ReadString('\n')
                lines++
                if err != nil {
                        break
                }
        }

        if err != io.EOF {
                return 0, err
        }
        return lines, nil
 }


Мы создаем bufio.Reader, затем сидим в цикле, вызывая метод ReadString, увеличивая счетчик до тех пор, пока не достигнем конца файла, а затем возвращаем количество прочитанных строк. Это код, который мы хотели написать, вместо этого CountLines усложняется обработкой ошибок.

Например, есть такая странная конструкция:

_, err = br.ReadString('\n')
lines++
if err != nil {
  break
}


Мы увеличиваем количество строк перед проверкой ошибки — это выглядит странно. Причина, по которой мы должны написать это таким образом, заключается в том, что ReadString вернет ошибку, если он встретит конец файла — io.EOF — до нажатия символа новой строки. Это может произойти также, если нет символа новой строки.

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

Но мы еще не закончили проверять ошибки. ReadString вернет io.EOF, когда достигнет конца файла. Это ожидаемо, ReadString нужен какой-то способ сказать стоп, больше нечего читать. Поэтому, прежде чем вернуть ошибку вызывающей стороне CountLine, нам нужно проверить, не была ли ошибка io.EOF, и в этом случае вернуть ее вызывающему, иначе мы вернем nil, когда все хорошо. Вот почему последняя строка функции не просто

return lines, err


Я думаю, что это хороший пример наблюдения Расса Кокса о том, что обработка ошибок может затруднить работу функции. Давайте посмотрим на улучшенную версию.

func CountLines(r io.Reader) (int, error) {
        sc := bufio.NewScanner(r)
        lines := 0

        for sc.Scan() {
                lines++
        }

        return lines, sc.Err()
}


Эта улучшенная версия переход от использования bufio.Reader к bufio.Scanner. Под капотом bufio.Scanner использует bufio.Reader, добавляя слой абстракции, который помогает убрать обработку ошибок, которая затруднила работу нашей предыдущей версии CountLines (bufio.Scanner может сканировать любой шаблон, по умолчанию он ищет новые строки).

Метод sc.Scan () возвращает true, если сканер нашел строку текста и не обнаружил ошибку. Таким образом, тело нашего цикла for будет вызываться только тогда, когда в буфере сканера есть строка текста. Это означает, что наша переделанная CountLines правильно обрабатывает случай, когда нет завершающего символа новой строки. Также теперь правильно обрабатывается случай, когда файл пуст.

Во-вторых, поскольку sc.Scan возвращает false при возникновении ошибки, наш цикл for завершится, когда будет достигнут конец файла или возникнет ошибка. Тип bufio.Scanner запоминает первую ошибку обнаруженную ошибку, и мы исправляем эту ошибку после выхода из цикла с помощью метода sc.Err ().

Наконец, buffo.Scanner позаботится об обработке io.EOF и преобразует его в nil, если конец файла достигнут без возникновения ошибки.

Пример второй


Мой второй пример вдохновлен записью Errors are values в блоге Роба Пайкса.

При работе с открытием, записью и закрытием файлов обработка ошибок есть, но не очень впечатляющая, поскольку операции могут быть заключены в помощники, такие как ioutil.ReadFile и ioutil.WriteFile. Однако при работе с сетевыми протоколами низкого уровня часто возникает необходимость в построении ответа напрямую с использованием примитивов ввода-вывода, поэтому обработка ошибок может начать повторятся. Рассмотрим этот фрагмент HTTP-сервера, который создает ответ HTTP / 1.1:

type Header struct {
        Key, Value string
}

type Status struct {
        Code   int
        Reason string
}

func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
        _, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
        if err != nil {
                return err
        }
        
        for _, h := range headers {
                _, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value)
                if err != nil {
                        return err
                }
        }

        if _, err := fmt.Fprint(w, "\r\n"); err != nil {
                return err
        } 

        _, err = io.Copy(w, body) 
        return err
}


Сначала мы создаем строку состояния, используя fmt.Fprintf, и проверяем ошибку. Затем для каждого заголовка мы записываем ключ и значение заголовка, каждый раз проверяя ошибку. Наконец, мы завершаем раздел заголовка дополнительным \r \n, проверяем ошибку и копируем тело ответа клиенту. Наконец, хотя нам не нужно проверять ошибку из io.Copy, нам нужно преобразовать ее из формы с двумя возвращаемыми значениями, которую io.Copy возвращает в одно возвращаемое значение, которое ожидает WriteResponse.

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

type errWriter struct {
        io.Writer
        err error
}

func (e *errWriter) Write(buf []byte) (int, error) {
        if e.err != nil {
                return 0, e.err
        }

        var n int
        n, e.err = e.Writer.Write(buf)
        return n, nil
}


errWriter выполняет контракт io.Writer, поэтому его можно использовать для переноса существующего io.Writer. errWriter передает записи своему базовому устройству записи до тех пор, пока не будет обнаружена ошибка. С этого момента он отбрасывает любые записи и возвращает предыдущую ошибку.

func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
        ew := &errWriter{Writer: w} 
        fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)

        for _, h := range headers {
                fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value)
        }

        fmt.Fprint(ew, "\r\n")
        io.Copy(ew, body)

        return ew.err
}


Применение errWriter к WriteResponse значительно улучшает ясность кода. Каждой из операций больше не нужно ограничивать себя проверкой ошибок. Сообщение об ошибке перемещается в конец функции, проверяя поле ew.err и избегая назойливого перевода возвращаемых значений io.Copy

Заключение


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

© Habrahabr.ru