Обработка ошибок в Go

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

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

Создание ошибок

Прежде чем мы сможем обрабатывать ошибки, нам нужно сначала их создать. Стандартная библиотека предоставляет две встроенные функции для создания ошибок:errors.New иfmt.Errorf. Обе эти функции позволяют вам указывать пользовательское сообщение об ошибке, которое вы можете позже представить своим пользователям.

errors.New принимает единственный аргумент - сообщение об ошибке в виде строки, которую вы можете настроить, чтобы предупредить пользователей о том, что пошло не так.

Попробуйте выполнить следующий пример, чтобы увидеть ошибку, созданнуюerrors.New, выводимую на стандартный вывод:

package main

import (
    "errors"
    "fmt"
)

func main() {
    err := errors.New("barnacles")
    fmt.Println("Sammy says:", err)
}
OutputSammy says: barnacles

Мы использовали функциюerrors.New из стандартной библиотеки, чтобы создать новое сообщение об ошибке со строкой"barnacles" в качестве сообщения об ошибке. Здесь мы следовали соглашению, используя строчные буквы для сообщения об ошибке, как предлагаетGo Programming Language Style Guide.

Наконец, мы использовали функциюfmt.Println, чтобы объединить наше сообщение об ошибке с"Sammy says:".

Функцияfmt.Errorf позволяет динамически создавать сообщение об ошибке. Его первым аргументом является строка, содержащая ваше сообщение об ошибке со значениями-заполнителями, такими как%s для строки и%d для целого числа. fmt.Errorf интерполирует аргументы, следующие за этой строкой форматирования, в эти заполнители в следующем порядке:

package main

import (
    "fmt"
    "time"
)

func main() {
    err := fmt.Errorf("error occurred at: %v", time.Now())
    fmt.Println("An error happened:", err)
}
OutputAn error happened: Error occurred at: 2019-07-11 16:52:42.532621 -0400 EDT m=+0.000137103

Мы использовали функциюfmt.Errorf, чтобы создать сообщение об ошибке, которое будет включать текущее время. Строка форматирования, которую мы предоставилиfmt.Errorf, содержит директиву форматирования%v, которая сообщаетfmt.Errorf использовать форматирование по умолчанию для первого аргумента, предоставленного после строки форматирования. Этот аргумент будет текущим временем, предоставленным функциейtime.Now из стандартной библиотеки. Как и в предыдущем примере, мы объединяем наше сообщение об ошибке с коротким префиксом и выводим результат на стандартный вывод с помощью функцииfmt.Println.

Обработка ошибок

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

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

package main

import (
    "errors"
    "fmt"
)

func boom() error {
    return errors.New("barnacles")
}

func main() {
    err := boom()

    if err != nil {
        fmt.Println("An error occurred:", err)
        return
    }
    fmt.Println("Anchors away!")
}
OutputAn error occurred: barnacles

Здесь мы определяем функцию с именемboom(), которая возвращает единственныйerror, который мы создаем с использованиемerrors.New. Затем мы вызываем эту функцию и фиксируем ошибку с помощью строкиerr := boom().
Как только мы назначаем эту ошибку, мы проверяем, присутствовала ли она с условным выражениемif err != nil. Здесь условное выражение всегда будет оцениваться какtrue, поскольку мы всегда возвращаемerror изboom().

Это не всегда так, поэтому рекомендуется иметь логическую обработку случаев, когда ошибки нет (nil), и случаев, когда ошибка присутствует. Когда ошибка присутствует, мы используемfmt.Println для печати нашей ошибки вместе с префиксом, как мы это делали в предыдущих примерах. Наконец, мы используем операторreturn, чтобы пропустить выполнениеfmt.Println("Anchors away!"), поскольку оно должно выполняться только в том случае, если ошибок не произошло.

[.note] #Note: Конструкцияif err != nil, показанная в последнем примере, - это рабочая лошадка для обработки ошибок в языке программирования Go. Везде, где функция может вызвать ошибку, важно использовать операторif, чтобы проверить, произошла ли она. Таким образом, идиоматический код Go естественно имеет свою логику“happy path” на первом уровне отступа и всю логику «печального пути» на втором уровне отступа.
#

Операторы if имеют необязательное предложение присваивания, которое можно использовать для упрощения вызова функции и обработки ее ошибок.

Запустите следующую программу, чтобы увидеть тот же результат, что и в нашем предыдущем примере, но на этот раз с использованием составного оператораif, чтобы уменьшить некоторый шаблон:

package main

import (
    "errors"
    "fmt"
)

func boom() error {
    return errors.New("barnacles")
}

func main() {
    if err := boom(); err != nil {
        fmt.Println("An error occurred:", err)
        return
    }
    fmt.Println("Anchors away!")
}
OutputAn error occurred: barnacles

Как и раньше, у нас есть функцияboom(), которая всегда возвращает ошибку. Мы назначаем ошибку, возвращаемуюboom(),err в качестве первой части оператораif. Во второй части оператораif после точки с запятой доступна переменнаяerr. Мы проверяем, присутствовала ли ошибка, и печатаем нашу ошибку с короткой префиксной строкой, как мы делали ранее.

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

Возврат ошибок наряду со значениями

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

Чтобы создать функцию, которая возвращает более одного значения, мы перечисляем типы каждого возвращаемого значения в скобках в сигнатуре функции. Например, функцияcapitalize, которая возвращаетstring иerror, будет объявлена ​​с использованиемfunc capitalize(name string) (string, error) {}. Часть(string, error) сообщает компилятору Go, что эта функция вернетstring иerror в указанном порядке.

Запустите следующую программу, чтобы увидеть выходные данные функции, которая возвращает какstring, так иerror:

package main

import (
    "errors"
    "fmt"
    "strings"
)

func capitalize(name string) (string, error) {
    if name == "" {
        return "", errors.New("no name provided")
    }
    return strings.ToTitle(name), nil
}

func main() {
    name, err := capitalize("sammy")
    if err != nil {
        fmt.Println("Could not capitalize:", err)
        return
    }

    fmt.Println("Capitalized name:", name)
}
OutputCapitalized name: SAMMY

Мы определяемcapitalize() как функцию, которая принимает строку (имя, которое следует писать с заглавной буквы) и возвращает строку и значение ошибки. Вmain() мы вызываемcapitalize() и присваиваем два значения, возвращаемые функцией, переменнымname иerr, разделяя их запятыми в левой части поля. Оператор:=. После этого мы выполняем нашу проверкуif err != nil, как в предыдущих примерах, выводя ошибку на стандартный вывод, используяfmt.Println, если ошибка присутствовала. Если ошибок не было, печатаемCapitalized name: SAMMY.

Попробуйте заменить строку"sammy" вname, err := capitalize("sammy") на пустую строку(""), и вместо этого вы получите ошибкуCould not capitalize: no name provided.

Функцияcapitalize вернет ошибку, когда вызывающие функции предоставят пустую строку для параметраname. Когда параметрname не является пустой строкой,capitalize() используетstrings.ToTitle для использования заглавной буквы в параметреname и возвращаетnil для значения ошибки.

Есть несколько тонких соглашений, которым следует этот пример, которые типичны для кода Go, но не соблюдаются компилятором Go. Когда функция возвращает несколько значений, включая ошибку, соглашение требует, чтобы мы возвращалиerror в качестве последнего элемента. При возвратеerror из функции с несколькими возвращаемыми значениями идиоматический код Go также установит каждое значение, не связанное с ошибкой, наzero value. Нулевые значения - это, например, пустая строка для строк,0 для целых чисел, пустая структура для типов структур иnil для типов интерфейсов и указателей, и многие другие. Мы рассмотрим нулевые значения более подробно в нашемtutorial on variables and constants.

Редукционный шаблон

Соблюдение этих соглашений может стать утомительным в ситуациях, когда есть много значений, которые нужно вернуть из функции. Мы можем использоватьanonymous function, чтобы уменьшить шаблон. Анонимные функции - это процедуры, назначаемые переменным. В отличие от функций, которые мы определили в предыдущих примерах, они доступны только в функциях, в которых вы их объявляете - это делает их идеальными для использования в качестве коротких фрагментов многократно используемой вспомогательной логики.

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

package main

import (
    "errors"
    "fmt"
    "strings"
)

func capitalize(name string) (string, int, error) {
    handle := func(err error) (string, int, error) {
        return "", 0, err
    }

    if name == "" {
        return handle(errors.New("no name provided"))
    }

    return strings.ToTitle(name), len(name), nil
}

func main() {
    name, size, err := capitalize("sammy")
    if err != nil {
        fmt.Println("An error occurred:", err)
    }

    fmt.Printf("Capitalized name: %s, length: %d", name, size)
}
OutputCapitalized name: SAMMY, length: 5

В пределахmain() мы теперь захватываем три возвращенных аргумента изcapitalize какname,size иerr соответственно. Затем мы проверяем, вернул лиcapitalizeerror, проверив, не была ли переменнаяerr равнаnil. Это важно сделать перед попыткой использовать какие-либо другие значения, возвращаемыеcapitalize, потому что анонимная функцияhandle может установить для них нулевые значения. Поскольку ошибки не произошло, поскольку мы предоставили строку"sammy", мы печатаем имя с заглавной буквы и его длину.

Еще раз, вы можете попробовать заменить"sammy" пустой строкой(""), чтобы увидеть напечатанный случай ошибки (An error occurred: no name provided).

Вcapitalize мы определяем переменнуюhandle как анонимную функцию. Он принимает одну ошибку и возвращает идентичные значения в том же порядке, что и возвращаемые значенияcapitalize. handle устанавливает эти значения в нулевые значения и пересылаетerror, переданный в качестве аргумента, в качестве окончательного возвращаемого значения. Используя это, мы можем затем вернуть любые ошибки, обнаруженные вcapitalize, используя операторreturn перед вызовомhandle сerror в качестве параметра.

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

Обработка ошибок из функций множественного возврата

Когда функция возвращает много значений, Go требует, чтобы мы присвоили каждое из них переменной. В последнем примере мы делаем это, предоставляя имена для двух значений, возвращаемых функциейcapitalize. Эти имена должны быть разделены запятыми и появиться слева от оператора:=. Первое значение, возвращаемое изcapitalize, будет присвоено переменнойname, а второе значение (error) будет присвоено переменнойerr. Иногда нас интересует только значение ошибки. Вы можете отбросить любые нежелательные значения, возвращаемые функциями, используя специальное имя переменной_.

В следующей программе мы изменили наш первый пример с функциейcapitalize, чтобы вывести ошибку, передав пустую строку(""). Попробуйте запустить эту программу, чтобы увидеть, как мы можем исследовать только ошибку, отбрасывая первое возвращенное значение с помощью переменной_:

package main

import (
    "errors"
    "fmt"
    "strings"
)

func capitalize(name string) (string, error) {
    if name == "" {
        return "", errors.New("no name provided")
    }
    return strings.ToTitle(name), nil
}

func main() {
    _, err := capitalize("")
    if err != nil {
        fmt.Println("Could not capitalize:", err)
        return
    }
    fmt.Println("Success!")
}
OutputCould not capitalize: no name provided

На этот раз внутри функцииmain() мы назначаем имя с заглавной буквы (string возвращается первым) переменной подчеркивания (_). В то же время мы присваиваемerror, возвращаемыйcapitalize, переменнойerr. Затем мы проверяем, присутствует ли ошибка в условном выраженииif err != nil. Поскольку мы жестко запрограммировали пустую строку в качестве аргументаcapitalize в строке_, err := capitalize(""), это условное выражение всегда будет оцениваться какtrue. Это производит вывод"Could not capitalize: no name provided", напечатанный вызовом функцииfmt.Println в теле оператораif. return после этого пропуститfmt.Println("Success!").

Заключение

Мы видели много способов создания ошибок с использованием стандартной библиотеки и создания функций, которые возвращают ошибки идиоматическим образом. В этом руководстве нам удалось успешно создать различные ошибки, используя функции стандартной библиотекиerrors.New иfmt.Errorf. В будущих руководствах мы рассмотрим, как создавать собственные типы ошибок, чтобы предоставить пользователям более подробную информацию.