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

Устойчивый и надежный код должен уметь правильно реагировать на неожиданные обстоятельства: например, на неправильный пользовательский ввод, сетевые сбои и ошибки диска. Обработка ошибок – это процесс обнаружения непредсказуемых состояний и записи диагностической информации для дальнейшего устранения неполадок.

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

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

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

Функция errors.New принимает один аргумент – сообщение об ошибке в виде строки, оно позволяет вам предупредить своих пользователей о том, что пошло не так.

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

package main
import (
"errors"
"fmt"
)
func main() {
err := errors.New("barnacles")
fmt.Println("An error occurred:", err)
}
An error occurred: barnacles

Мы использовали функцию errors.New из стандартной библиотеки, чтобы создать новое сообщение об ошибке со строкой «barnacles» в качестве сообщения об ошибке. При этом мы следовали соглашению, используя строчные буквы, как предлагает руководство по стилю Go.

Затем мы использовали функцию fmt.Println, чтобы объединить наше сообщение об ошибке со строкой «An error occurred:».

Функция 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)
}
An error happened: Error occurred at: 2019-07-11 16:52:42.532621 -0400 EDT m=+0.000137103

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

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

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

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

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!")
}
An error occurred: barnacles

Здесь мы определили функцию boom(), которая возвращает единичную ошибку, созданную с помощью error.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!")
}
An error occurred: barnacles

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

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

Функции, которые возвращают единичную ошибку, часто влияют на некоторые stateful изменения (такие как вставка строк в базу данных). Также часто используются функции, которые возвращают значение в случае успешного выполнения и потенциальную ошибку в обратном случае. 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("myname")
if err != nil {
fmt.Println("Could not capitalize:", err)
return
}
fmt.Println("Capitalized name:", name)
}
Capitalized name: MYNAME

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

Попробуйте заменить строку «myname» в name, err := capitalize(«myname») на пустую строку («»), и вы получите сообщение об ошибке.

Could not capitalize: no name provided

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

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

Читайте также: Переменные и константы в Go

 Уменьшение шаблона

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

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

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("myname")
if err != nil {
fmt.Println("An error occurred:", err)
}
fmt.Printf("Capitalized name: %s, length: %d", name, size)
}
Capitalized name: MYNAME, length: 6

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

Тут вы снова можете попробовать изменить «myname» на пустую строку («»), чтобы увидеть  ошибку:

An error occurred: no name provided

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

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

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

Когда функция возвращает много значений, Go требует, чтобы мы присвоили каждое из них переменной. В последнем примере мы так и сделали. При этом имена должны быть разделены запятыми и отображаться слева от оператора :=. Первое значение, возвращаемое 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!")
}
Could not capitalize: no name provided

В этот раз в функции main() мы присвоили имя capitalized переменной _. Затем мы присвоили 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. В следующих уроках мы рассмотрим, как создавать собственные типы ошибок, чтобы предоставить пользователям более подробную информацию.

Tags: ,