Хороший код должен правильно реагировать на непредвиденные обстоятельства, такие как ввод некорректных данных пользователем, разрыв сетевого подключения или отказ дисков. Обработка ошибок — это процесс обнаружения ситуаций, когда ваша программа находится в неожиданном состоянии, а также принятие мер для записи диагностической информации, которая будет полезна при последующей отладке.
В отличие от других языков программирования, где разработчикам нужно обрабатывать ошибки с помощью специального синтаксиса, ошибки в 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.
Наконец, мы использовали функцию 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!")
, поскольку этот код следует выполнять только при отсутствии ошибок.
Примечание: конструкция if err ! = nil
, показанная в последнем примере, является стандартной практикой обработки ошибок в языке программирования Go. Если функция может генерировать ошибку, важно использовать оператор if
, чтобы проверить наличие ошибки. Таким образом, код Go естественным образом имеет логику "happy path"на первом уровне условия и логику "sad 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
. Эта переменная err
будет доступна во второй части оператора if
после точки с запятой. Мы должны убедиться в наличии ошибки и вывести нашу ошибку с коротким префиксом, как мы уже делали до этого.
В этом разделе мы научились обрабатывать функции, которые возвращают только ошибки. Подобные функции распространены широко, но также важно иметь возможность обрабатывать ошибки из функций, которые могут возвращать несколько значений.
Функции, возвращающие одно значение ошибки, часто относятся к функциям, выполняющим изменения с сохранением состояния, например, вставляющим строки в базу данных. Также вы можете написать функции, возвращающие значение при успешном завершении работы и ошибку, если работа функции завершилась сбоем. Go позволяет функциям возвращать более одного результата, т. е. они могут использоваться для возврата как значения, так и типа ошибки.
Чтобы создать функцию, которая возвращает несколько значений, мы перечислим типы всех возвращаемых значений внутри скобок в сигнатуре функции. Например, функция capitalize
, которая возвращает string
и error
, будет объявлена следующим образом: func capitalize(name string) (string, error) {}
. Часть (string, error)
сообщает компилятору Go, что эта функция возвращает строку
и ошибку
в указанном порядке.
Запустите следующую программу, чтобы увидеть вывод функции, которая возвращает 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
последним элементом. При возвращении ошибки
функцией с несколькими возвращаемыми значениями, идиоматический код Go также устанавливает для любого значения, не являющегося ошибкой, нулевое значение. Нулевое значение — это, например, пустая строка для string, 0
для целых чисел, пустая структура для структур и nil
для интерфейса и типов указателя и т. д. Мы более подробно познакомимся с нулевыми значениями в нашем руководстве по переменным и константам.
Соблюдение этих конвенций может стать трудновыполнимой задачей в ситуациях, когда существует множество значений, возвращаемых функцией. Мы можем использовать анонимную функцию для сокращения объема кода. Анонимные функции — это процедуры для переменных. В отличие от функций, описанных в предыдущих примерах, они доступны только в функциях, где вы их объявили, что делает их идеальным инструментом для использования в коротких элементах вспомогательной логики.
Следующая программа изменяет последний пример, чтобы включить длину имени, которое мы будем переводить в верхний регистр. Поскольку функция возвращает три значения, обработка ошибок может стать громоздкой без анонимной функции, которая может нам помочь:
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
. Затем мы проверим, возвращает ли capitalize
error
, убедившись, что переменная 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()
на этот раз мы присвоим состоящее из заглавных букв имя (строка
, возвращаемая первой) переменной с нижним подчеркиванием (_
). В то же самое время мы присваиваем 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
стандартной библиотеки. В будущих руководствах мы рассмотрим, как создавать собственные типы ошибок для предоставления более полной информации пользователям.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!