-
Documentation
-
Tutorials
-
Return and handle an error
Handling errors is an essential feature of solid code. In this section, you’ll
add a bit of code to return an error from the greetings module, then handle it
in the caller.
-
In greetings/greetings.go, add the code highlighted below.
There’s no sense sending a greeting back if you don’t know who to greet.
Return an error to the caller if the name is empty. Copy the following
code into greetings.go and save the file.package greetings import ( "errors" "fmt" ) // Hello returns a greeting for the named person. func Hello(name string) (string, error) { // If no name was given, return an error with a message. if name == "" { return "", errors.New("empty name") } // If a name was received, return a value that embeds the name // in a greeting message. message := fmt.Sprintf("Hi, %v. Welcome!", name) return message, nil }
In this code, you:
-
Change the function so that it returns two values: a
string
and anerror
. Your caller will check
the second value to see if an error occurred. (Any Go function can
return multiple values. For more, see
Effective Go.) -
Import the Go standard library
errors
package so you can
use its
errors.New
function. -
Add an
if
statement to check for an invalid request (an
empty string where the name should be) and return an error if the
request is invalid. Theerrors.New
function returns an
error
with your message inside. -
Add
nil
(meaning no error) as a second value in the
successful return. That way, the caller can see that the function
succeeded.
-
Change the function so that it returns two values: a
-
In your hello/hello.go file, handle the error now returned by the
Hello
function, along with the non-error value.Paste the following code into hello.go.
package main import ( "fmt" "log" "example.com/greetings" ) func main() { // Set properties of the predefined Logger, including // the log entry prefix and a flag to disable printing // the time, source file, and line number. log.SetPrefix("greetings: ") log.SetFlags(0) // Request a greeting message. message, err := greetings.Hello("") // If an error was returned, print it to the console and // exit the program. if err != nil { log.Fatal(err) } // If no error was returned, print the returned message // to the console. fmt.Println(message) }
In this code, you:
-
Configure the
log
package to
print the command name («greetings: «) at the start of its log messages,
without a time stamp or source file information. -
Assign both of the
Hello
return values, including the
error
, to variables. -
Change the
Hello
argument from Gladys’s name to an empty
string, so you can try out your error-handling code. -
Look for a non-nil
error
value. There’s no sense continuing
in this case. -
Use the functions in the standard library’s
log package
to
output error information. If you get an error, you use the
log
package’s
Fatal
function
to print the error and stop the program.
-
Configure the
-
At the command line in the
hello
directory, run hello.go to
confirm that the code works.Now that you’re passing in an empty name, you’ll get an error.
$ go run . greetings: empty name exit status 1
That’s common error handling in Go: Return an error as a value so the caller
can check for it.
Next, you’ll use a Go slice to return a randomly-selected greeting.
< Call your code from another module
Return a random greeting >
Привет, хабровчане! Уже сегодня в ОТУС стартует курс «Разработчик Golang» и мы считаем это отличным поводом, чтобы поделиться еще одной полезной публикацией по теме. Сегодня поговорим о подходе Go к ошибкам. Начнем!
Освоение прагматической обработки ошибок в вашем Go-коде
Этот пост является частью серии «Перед тем как приступать к Go», где мы исследуем мир Golang, делимся советами и идеями, которые вы должны знать при написании кода на Go, чтобы вам не пришлось набивать собственные шишки.
Я предполагаю, что у вас уже имеется хотя бы базовый опыт работы с Go, но если вы чувствуете, что в какой-то момент вы столкнулись с незнакомым обсуждаемым материалом, не стесняйтесь делать паузу, исследовать тему и возвращаться.
Теперь, когда мы расчистили себе путь, поехали!
Подход Go к обработке ошибок — одна из самых спорных и неправильно используемых фич. В этой статье вы узнаете подход Go к ошибкам, и поймете, как они работают “под капотом”. Вы изучите несколько различных подходов, рассмотрите исходный код Go и стандартную библиотеку, чтобы узнать, как обрабатываются ошибки и как с ними работать. Вы узнаете, почему утверждения типа (Type Assertions) играют важную роль в их обработке, и увидите предстоящие изменения в обработке ошибок, которые планируется ввести в Go 2.
Вступление
Сперва-наперво: ошибки в Go не являются исключениями. Дэйв Чейни написал эпический блогпост об этом, поэтому я отсылаю вас к нему и резюмирую: на других языках вы не можете быть уверены, может ли функция вызвать исключение или нет. Вместо генерации исключений функции Go поддерживают множественные возвращаемые значения, и по соглашению эта возможность обычно используется для возврата результата функции наряду с переменной ошибки.
Если по какой-то причине ваша функция может дать сбой, вам, вероятно, следует вернуть из нее предварительно объявленный error
-тип. По соглашению, возврат ошибки сигнализирует вызывающей стороне о проблеме, а возврат nil не считается ошибкой. Таким образом, вы дадите вызывающему понять, что возникла проблема, и ему нужно разобраться с ней: кто бы ни вызвал вашу функцию, он знает, что не должен полагаться на результат до проверки на наличие ошибки. Если ошибка не nil, он обязан проверить ее и обработать (логировать, возвращать, обслуживать, вызвать какой-либо механизм повторной попытки/очистки и т. д.).
(3 // обработка ошибки
5 // продолжение)
Эти фрагменты очень распространены в Go, и некоторые рассматривают их в качестве шаблонного кода. Компилятор рассматривает неиспользуемые переменные как ошибки компиляции, поэтому, если вы не собираетесь проверять наличие ошибок, вы должны назначить их пустому идентификатору. Но как бы удобно это ни было, ошибки не следует игнорировать.
( 4 // игнорирование ошибок небезопасно, и вы не должны полагаться на результат прежде, чем проверите наличие ошибок)
результату нельзя доверять до проверки на наличие ошибок
Возврат ошибки вместе с результатами, наряду со строгой системой типов Go, значительно усложняет написание забагованного кода. Вы всегда должны полагать, что значение функции повреждено, если только вы не проверили ошибку, которую она возвратила, и, присваивая ошибку пустому идентификатору, вы явно игнорируете, что значение вашей функции может быть повреждено.
Пустой идентификатор темен и полон ужасов.
У Go действительно есть panic
и recover
механизмы, которые также описаны в другом подробном посте в блоге Go. Но они не предназначены для имитации исключений. По словам Дейва, «Когда вы паникуете в Go — вы действительно паникуете: это не проблема кого-то другого, это уже геймовер». Они фатальны и приводят к сбою в вашей программе. Роб Пайк придумал поговорку «Не паникуйте», которая говорит сама за себя: вам, вероятно, следует избегать эти механизмы и вместо них возвращать ошибки.
«Ошибки — значения».
«Не просто проверяйте наличие ошибок, а элегантно их обрабатывайте»
«Не паникуйте»
все поговорки Роба Пайка
Под капотом
Интерфейс ошибки
Под капотом тип error — это простой интерфейс с одним методом, и если вы с ним не знакомы, я настоятельно рекомендую просмотреть этот пост в официальном блоге Go.
интерфейс error из исходного кода
Свои собственные ошибки реализовать не сложно. Существуют различные подходы к пользовательским структурам, реализующим метод Error()
string
. Любая структура, реализующая этот единственный метод, считается допустимым значением ошибки и может быть возвращена как таковая.
Давайте рассмотрим несколько таких подходов.
Встроенная структура errorString
Наиболее часто используемая и широко распространенная реализация интерфейса ошибок — это встроенная структура errorString
. Это самая простая реализация, о которой вы только можете подумать.
Источник: исходный код Go
Вы можете лицезреть ее упрощенную реализацию здесь. Все, что она делает, это содержит string
, и эта строка возвращается методом Error
. Эта стринговая ошибка может быть нами отформатирована на основе некоторых данных, скажем, с помощью fmt.Sprintf
. Но кроме этого, она не содержит никаких других возможностей. Если вы применили errors.New или fmt.Errorf, значит вы уже использовали ее.
(13// вывод:)
попробуйте
github.com/pkg/errors
Другой простой пример — пакет pkg/errors. Не путать со встроенным пакетом errors
, о котором вы узнали ранее, этот пакет предоставляет дополнительные важные возможности, такие как обертка ошибок, развертка, форматирование и запись стек-трейса. Вы можете установить пакет, запустив go get github.com/pkg/errors
.
В тех случаях, когда вам нужно прикрепить стек-трейс или необходимую информацию об отладке к вашим ошибкам, использование функций New
или Errorf
этого пакета предоставляет ошибки, которые уже записываются в ваш стек-трейс, и вы так же можете прикрепить простые метаданные, используя его возможности форматирования. Errorf
реализует интерфейс fmt.Formatter, то есть вы можете отформатировать его, используя руны пакета fmt
( %s
, %v
, %+v
и т. д.).
(//6 или альтернатива)
Этот пакет также представляет функции errors.Wrap
и errors.Wrapf
. Эти функции добавляют контекст к ошибке с помощью сообщения и стек-трейса в том месте, где они были вызваны. Таким образом, вместо простого возврата ошибки, вы можете обернуть ее контекстом и важными отладочными данными.
Обертки ошибок другими ошибками поддерживают Cause() error
метод, который возвращает их внутреннюю ошибку. Кроме того, они могут использоваться сerrors.Cause(err error) error
функцией, которая извлекает основную внутреннюю ошибку в оборачивающей ошибке.
Работа с ошибками
Утверждение типа
Утверждения типа (Type Assertion) играют важную роль при работе с ошибками. Вы будете использовать их для извлечения информации из интерфейсного значения, а поскольку обработка ошибок связана с пользовательскими реализациями интерфейса error
, реализация утверждений на ошибках является очень удобным инструментом.
Его синтаксис одинаков для всех его целей — x.(T)
, если x
имеет тип интерфейса. x.(T)
утверждает, что x
не равен nil
и что значение, хранящееся в x
, относится к типу T
. В следующих нескольких разделах мы рассмотрим два способа использования утверждений типа — с конкретным типом T
и с интерфейсом типа T
.
(2//сокращенный синтаксис, пропускающий логическую переменную ok
3//паника: преобразование интерфейса: интерфейс {} равен nil, а не string
6//удлиненный синтаксис с логической переменной ok
8//не паникует, вместо этого присваивает ok false, когда утверждение ложно
9// теперь мы можем безопасно использовать s как строку)
песочница: panic при укороченном синтаксисе, безопасный удлинённый синтаксис
Дополнительное примечание, касающееся синтаксиса: утверждение типа может использоваться как с укороченным синтаксисом (который паникует при неудачном утверждении), так и с удлиненным синтаксисом (который использует логическое значение OK для указания успеха или неудачи). Я всегда рекомендую брать удлиненный вместо укороченного, так как я предпочитаю проверять переменную OK, а не разбираться с паникой.
Утверждение с интерфейсом типа T
Выполнение утверждения типа x.(T)
с интерфейсом типа T
подтверждает, что x
реализует интерфейс T
. Таким образом, вы можете гарантировать, что интерфейсное значение реализует интерфейс, и только если это так, вы сможете использовать его методы.
(5…// утверждаем, что x реализует интерфейс resolver
6…// здесь мы уже можем безопасно использовать этот метод)
Чтобы понять, как это можно использовать, давайте снова взглянем на pkg/errors
. Вы уже знаете этот пакет ошибок, так что давайте углубимся в errors.Cause(err error) error
функцию.
Эта функция получает ошибку и извлекает самую внутреннюю ошибку, которую та переносит (ту, которая уже не служит оберткой для другой ошибки). Это может показаться примитивным, но есть много замечательных вещей, которые вы можете извлечь из этой реализации:
источник: pkg/errors
Функция получает значение ошибки, и она не может предполагать, что получаемый ею err
аргумент является ошибкой-оберткой (поддерживаемой Cause
методом). Поэтому перед вызовом метода Cause
необходимо убедиться, что вы имеете дело с ошибкой, которая реализует этот метод. Выполняя утверждение типа в каждой итерации цикла for, вы можете убедиться, что cause
переменная поддерживает метод Cause
, и может продолжать извлекать из него внутренние ошибки до тех пор, пока не найдете ошибку, у которой нет Cause
.
Создавая простой локальный интерфейс, содержащий только те методы, которые вам нужны, и применяя на нем утверждение, ваш код отделен от других зависимостей. Полученный вами аргумент не обязательно должен быть известной структурой, он просто должен быть ошибкой. Любой тип, реализующий методы Error
и Cause
, подойдет. Таким образом, если вы реализуете Cause
метод в своем типе ошибки, вы можете использовать эту функцию с ним без замедлений.
Однако следует помнить об одном небольшом недостатке: интерфейсы могут изменяться, поэтому вам следует тщательно поддерживать код, чтобы ваши утверждения не нарушались. Не забудьте определить свои интерфейсы там, где вы их используете, поддерживать их стройными и аккуратными, и у вас все будет хорошо.
Наконец, если вам нужен только один метод, иногда удобнее сделать утверждение на анонимном интерфейсе, содержащем только метод, на который вы полагаетесь, т. е. v, ok := x.(interface{ F() (int, error) })
. Использование анонимных интерфейсов может помочь отделить ваш код от возможных зависимостей и защитить его от возможных изменений в интерфейсах.
Утверждение с конкретным типом T и Type Switch
Я предваряю этот раздел введением двух похожих шаблонов обработки ошибок, которые страдают от нескольких недостатков и ловушек. Это не значит, что они не распространенные. Оба они могут быть удобными инструментами в небольших проектах, но они плохо масштабируются.
Первый — это второй вариант утверждения типа: выполняется утверждение типа x.(T)
с конкретным типом T
. Он утверждает, что значение x
имеет тип T
, или оно может быть преобразовано в тип T
.
(2//мы можем использовать v как mypkg.SomeErrorType)
Другой — это шаблон Type Switch. Type Switch объединяют оператор switch с утверждением типа, используя зарезервированное ключевое слово type
. Они особенно распространены в обработке ошибок, где знание основного типа переменной ошибки может быть очень полезным.
(3// обработка…
5// обработка…)
Большим недостатком обоих подходов является то, что оба они приводят к связыванию кода со своими зависимостями. Оба примера должны быть знакомы со структурой SomeErrorType
(которая, очевидно, должна быть экспортирована) и должны импортировать пакет mypkg
.
В обоих подходах при обработке ваших ошибок вы должны быть знакомы с типом и импортировать его пакет. Ситуация усугубляется, когда вы имеете дело с ошибками в обертках, где причиной ошибки может быть ошибка, возникшая из-за внутренней зависимости, о которой вы не знаете и не должны знать.
(7// обработка…
9// обработка…)
Type Switch различают *MyStruct
и MyStruct
. Поэтому, если вы не уверены, имеете ли вы дело с указателем или фактическим экземпляром структуры, вам придется предоставить оба варианта. Более того, как и в случае с обычными switch, кейсы в Type Switch не проваливаются, но в отличие от обычных Type Switch, использование fallthrough
запрещено в Type Switch, поэтому вам придется использовать запятую и предоставлять обе опции, что легко забыть.
Подытожим
Вот и все! Теперь вы знакомы с ошибками и должны быть готовы к устранению любых ошибок, которые ваше приложение Go может выбросить (или фактически вернуть) на ваш путь!
Оба пакета errors
представляют простые, но важные подходы к ошибкам в Go, и, если они удовлетворяют вашим потребностям, они являются отличным выбором. Вы можете легко реализовать свои собственные структуры ошибок и пользоваться преимуществами обработки ошибок Go, комбинируя их с pkg/errors
.
Когда вы масштабируете простые ошибки, правильное использование утверждений типа может быть отличным инструментом для обработки различных ошибок. Либо с помощью Type Switch, либо путем утверждения поведения ошибки и проверки интерфейсов, которые она реализует.
Что дальше?
Обработка ошибок в Go сейчас очень актуальна. Теперь, когда вы получили основы, вам может быть интересно, что ждет нас в будущем для обработки ошибок Go!
В следующей версии Go 2 этому уделяется много внимания, и вы уже можете взглянуть на черновой вариант. Кроме того, во время dotGo 2019 Марсель ван Лохуизен провел отличную беседу на тему, которую я просто не могу не рекомендовать — «Значения ошибок GO 2 уже сегодня».
Очевидно, есть еще множество подходов, советов и хитростей, и я не могу включить их все в один пост! Несмотря на это, я надеюсь, что вам он понравился, и я увижу вас в следующем выпуске серии «Перед тем как приступать к Go»!
А теперь традиционно ждем ваши комментарии.
Обработка ошибок — важная особенность надежного кода. В этом разделе вы добавите немного кода, чтобы вернуть ошибку из модуля greetings, а затем обработать ее в вызывающей программе.
Примечание. Этот раздел является частью руководства, состоящего из нескольких частей, который начинается с создания модуля Go.
В greetings/greetings.go добавьте код, выделенный ниже.
Нет смысла отправлять приветствие в ответ, если вы не знаете, кого приветствовать. Вернуть вызывающему абоненту сообщение об ошибке, если имя пусто. Скопируйте следующий код в greetings.go и сохраните файл.
package greetings
import (
"errors"
"fmt"
)
// Hello возвращает приветствие для указанного человека.
func Hello(name string) (string, error) {
// Если имя не было указано, возвращаем ошибку с сообщением.
if name == "" {
return "", errors.New("empty name")
}
// Если имя было получено, возвращаем значение,
// включающее имя в приветственном сообщении.
message := fmt.Sprintf("Hi, %v. Welcome!", name)
return message, nil
}
В этом коде вы:
- Измените функцию так, чтобы она возвращала два значения: строку и ошибку. Ваш абонент проверит второе значение, чтобы увидеть, произошла ли ошибка. (Любая функция Go может возвращать несколько значений.)
- Импортируете пакет errors стандартной библиотеки Go, чтобы вы могли использовать его errors.New функцию.
- Добавляете оператор if, чтобы проверить недействительный запрос (пустую строку, где должно быть имя) и вернуть ошибку, если запрос недействителен. Функция errors.New возвращает ошибку с вашим сообщением внутри.
- Добавляете nil (что означает отсутствие ошибки) в качестве второго значения в успешном возврате. Таким образом, вызывающий может увидеть, что функция выполнена успешно.
В файле hello/hello.go обработайте ошибку, возвращаемую функцией Hello, вместе со значением, не связанным с ошибкой.
Вставьте следующий код в hello.go.
package main
import (
"fmt"
"log"
"example.com/greetings"
)
func main() {
// Устанавливаем свойства предопределенного Logger, включая
// префикс записи журнала и флаг отключения печати
// время, исходный файл и номер строки.
log.SetPrefix("greetings: ")
log.SetFlags(0)
// Запрос приветственного сообщения.
message, err := greetings.Hello("")
// Если вернулась ошибка, выводим ее в консоль и
// выходим из программы.
if err != nil {
log.Fatal(err)
}
// Если ошибок не было, распечатываем возвращенное сообщение
// в консоль.
fmt.Println(message)
}
В этом коде вы:
- Настроете пакет log для печати имени команды («greetings: «) в начале сообщений журнала без отметки времени или информации об исходном файле.
- Присвоете переменным оба возвращаемых значения Hello, включая ошибку.
- Измените аргумент Hello с имени Gladys на пустую строку, чтобы вы могли опробовать свой код обработки ошибок.
- Найдете значение ошибки, отличное от нуля. В этом случае нет смысла продолжать.
- Используете функции из пакета log стандартной библиотеки для вывода информации об ошибках. Если вы получили сообщение об ошибке, вы используете функцию Fatal пакета log, чтобы распечатать ошибку и остановить программу.
В командной строке в каталоге hello запустите hello.go, чтобы убедиться, что код работает.
Теперь, когда вы передаете пустое имя, вы получите сообщение об ошибке.
$ go run .
greetings: empty name
exit status 1
Это обычная обработка ошибок в Go: возвращать ошибку как значение, чтобы вызывающий мог ее проверить.
Далее, в следующем посте, вы будете использовать срез Go, чтобы вернуть случайно выбранное приветствие.
Читайте также:
- Создание модуля в Golang
- Создание модуля в Golang: вызов своего кода из другого модуля
- Создание модуля в Golang: возврат случайного приветствия
- Создание модуля в Golang: ответные приветствия для нескольких человек
- Создание модуля в Golang: добавить тест
- Создание модуля в Golang: скомпилируйте и установите приложение
- Управление зависимостями в Golang
Канал в Телеграм
Рассказываем и показываем на практических примерах, как обрабатывать различные ошибки в языке программирования Go.
Механизм обработки ошибок в Go отличается от обработки исключений в большинстве языков программирования, ведь в Golang ошибки исключениями не являются. Если говорить в целом, то ошибка в Go — это возвращаемое значение с типомerror
, которое демонстрирует сбой. А с точки зрения кода — интерфейс. В качестве ошибки может выступать любой объект, который этому интерфейсу удовлетворяет.
Выглядит это так:
type error interface {
Error() string
}
В данной статье мы рассмотрим наиболее популярные способы работы с ошибками в Golang.
- Как обрабатывать ошибки в Go?
- Создание ошибок
- Оборачивание ошибок
- Проверка типов с Is и As
- Сторонние пакеты по работе с ошибками в Go
- Defer, panic and recover
- После изложенного
Как обрабатывать ошибки в Go?
Чтобы обработать ошибку в Golang, необходимо сперва вернуть из функции переменную с объявленным типом error
и проверить её на nil
:
if err != nil {
return err
}
Если метод возвращает ошибку, значит, потенциально в его работе может возникнуть проблема, которую нужно обработать. В качестве реализации обработчика может выступать логирование ошибки или более сложные сценарии. Например, переоткрытие установленного сетевого соединения, повторный вызов метода и тому подобные операции.
Если метод возвращает разные типы ошибок, то их нужно проверять отдельно. То есть сначала происходит определение ошибки, а потом для каждого типа пишется свой обработчик.
В Go ошибки возвращаются и проверяются явно. Разработчик сам определяет, какие ошибки метод может вернуть, и реализовать их обработку на вызывающей стороне.
Создание ошибок
Перед тем как обработать ошибку, нужно её создать. В стандартной библиотеке для этого есть две встроенные функции — обе позволяют указывать и отображать сообщение об ошибке:
errors.New
fmt.Errorf
Метод errors.New()
создаёт ошибку, принимая в качестве параметра текстовое сообщение.
package main
import (
"errors"
"fmt"
)
func main() {
err := errors.New("emit macho dwarf: elf header corrupted")
fmt.Print(err)
}
С помощью метода fmt.Errorf
можно добавить дополнительную информацию об ошибке. Данные будут храниться внутри одной конкретной строки.
package main
import (
"fmt"
)
func main() {
const name, id = "bueller", 17
err := fmt.Errorf("user %q (id %d) not found", name, id)
fmt.Print(err)
}
Такой способ подходит, если эта дополнительная информация нужна только для логирования на вызывающей стороне. Если же с ней предстоит работать, можно воспользоваться другими механизмами.
Оборачивание ошибок
Поскольку Error
— это интерфейс, можно создать удовлетворяющую ему структуру с собственными полями. Тогда на вызывающей стороне этими самыми полями можно будет оперировать.
package main
import (
"fmt"
)
type NotFoundError struct {
UserId int
}
func (err NotFoundError) Error() string {
return fmt.Sprintf("user with id %d not found", err.UserId)
}
func SearchUser(id int) error {
// some logic for search
// ...
// if not found
var err NotFoundError
err.UserId = id
return err
}
func main() {
const id = 17
err := SearchUser(id)
if err != nil {
fmt.Println(err)
//type error checking
notFoundErr, ok := err.(NotFoundError)
if ok {
fmt.Println(notFoundErr.UserId)
}
}
}
Представим другую ситуацию. У нас есть метод, который вызывает внутри себя ещё один метод. В каждом из них проверяется своя ошибка. Иногда требуется в метод верхнего уровня передать сразу обе эти ошибки.
В Go есть соглашение о том, что ошибка, которая содержит внутри себя другую ошибку, может реализовать метод Unwrap
, который будет возвращать исходную ошибку.
Также для оборачивания ошибок в fmt.Errorf
есть плейсхолдер %w
, который и позволяет произвести такую упаковку.:
package main
import (
"errors"
"fmt"
"os"
)
func main() {
err := openFile("non-existing")
if err != nil {
fmt.Println(err.Error())
// get internal error
fmt.Println(errors.Unwrap(err))
}
}
func openFile(filename string) error {
if _, err := os.Open(filename); err != nil {
return fmt.Errorf("error opening %s: %w", filename, err)
}
return nil
}
Проверка типов с Is и As
В Go 1.13 в пакете Errors появились две функции, которые позволяют определить тип ошибки — чтобы написать тот или иной обработчик:
errors.Is
errors.As
Метод errors.Is
, по сути, сравнивает текущую ошибку с заранее заданным значением ошибки:
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func main() {
if _, err := os.Open("non-existing"); err != nil {
if errors.Is(err, fs.ErrNotExist) {
fmt.Println("file does not exist")
} else {
fmt.Println(err)
}
}
}
Если это будет та же самая ошибка, то функция вернёт true
, если нет — false
.
errors.As
проверяет, относится ли ошибка к конкретному типу (раньше надо было явно приводить тип ошибки к тому типу, который хотим проверить):
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func main() {
if _, err := os.Open("non-existing"); err != nil {
var pathError *fs.PathError
if errors.As(err, &pathError) {
fmt.Println("Failed at path:", pathError.Path)
} else {
fmt.Println(err)
}
}
}
Помимо прочего, эти методы удобны тем, что упрощают работу с упакованными ошибками, позволяя проверить каждую из них за один вызов.
Сторонние пакеты по работе с ошибками в Go
Помимо стандартного пакета Go, есть различные внешние библиотеки, которые расширяют функционал. При принятии решения об их использовании следует отталкиваться от задачи — использование может привести к падению производительности.
В качестве примера можно посмотреть на пакет pkg/errors
. Одной из его способностей является логирование stack trace:
package main
import (
"fmt"
"github.com/pkg/errors"
)
func main() {
err := errors.Errorf("whoops: %s", "foo")
fmt.Printf("%+v", err)
}
// Example output:
// whoops: foo
// github.com/pkg/errors_test.ExampleErrorf
// /home/dfc/src/github.com/pkg/errors/example_test.go:101
// testing.runExample
// /home/dfc/go/src/testing/example.go:114
// testing.RunExamples
// /home/dfc/go/src/testing/example.go:38
// testing.(*M).Run
// /home/dfc/go/src/testing/testing.go:744
// main.main
// /github.com/pkg/errors/_test/_testmain.go:102
// runtime.main
// /home/dfc/go/src/runtime/proc.go:183
// runtime.goexit
// /home/dfc/go/src/runtime/asm_amd64.s:2059
Defer, panic and recover
Помимо ошибок, о которых позаботился разработчик, в Go существуют аварии (похожи на исключительные ситуации, например, в Java). По сути, это те ошибки, которые разработчик не предусмотрел.
При возникновении таких ошибок Go останавливает выполнение программы и начинает раскручивать стек вызовов до тех пор, пока не завершит работу приложения или не найдёт функцию обработки аварии.
Для работы с такими ошибками существует механизм «defer, panic, recover»
Defer
Defer
помещает все вызовы функции в стек приложения. При этом отложенные функции выполняются в обратном порядке — независимо от того, вызвана паника или нет. Это бывает полезно при очистке ресурсов:
package main
import (
"fmt"
"os"
)
func main() {
f := createFile("/tmp/defer.txt")
defer closeFile(f)
writeFile(f)
}
func createFile(p string) *os.File {
fmt.Println("creating")
f, err := os.Create(p)
if err != nil {
panic(err)
}
return f
}
func writeFile(f *os.File) {
fmt.Println("writing")
fmt.Fprintln(f, "data")
}
func closeFile(f *os.File) {
fmt.Println("closing")
err := f.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
Panic
Panic
сигнализирует о том, что код не может решить текущую проблему, и останавливает выполнение приложения. После вызова оператора выполняются все отложенные функции, и программа завершается с сообщением о причине паники и трассировки стека.
Например, Golang будет «паниковать», когда число делится на ноль:
panic: runtime error: integer divide by zero
goroutine 1 [running]:
main.divide(0x0)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:16 +0xe6
main.divide(0x1)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x2)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x3)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x4)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x5)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.main()
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:11 +0x31
exit status 2
Также панику можно вызвать явно с помощью метода panic()
. Обычно его используют на этапе разработки и тестирования кода — а в конечном варианте убирают.
Recover
Эта функция нужна, чтобы вернуть контроль при панике. В таком случае работа приложения не прекращается, а восстанавливается и продолжается в нормальном режиме.
Recover всегда должна вызываться в функции defer
. Чтобы сообщить об ошибке как возвращаемом значении, вы должны вызвать функцию recover в той же горутине, что и паника, получить структуру ошибки из функции восстановления и передать её в переменную:
package main
import (
"errors"
"fmt"
)
func A() {
defer fmt.Println("Then we can't save the earth!")
defer func() {
if x := recover(); x != nil {
fmt.Printf("Panic: %+v\n", x)
}
}()
B()
}
func B() {
defer fmt.Println("And if it keeps getting hotter...")
C()
}
func C() {
defer fmt.Println("Turn on the air conditioner...")
Break()
}
func Break() {
defer fmt.Println("If it's more than 30 degrees...")
panic(errors.New("Global Warming!!!"))
}
func main() {
A()
}
После изложенного
Можно ли игнорировать ошибки? В теории — да. Но делать это нежелательно. Во-первых, наличие ошибки позволяет узнать, успешно ли выполнился метод. Во-вторых, если метод возвращает полезное значение и ошибку, то, не проверив её, нельзя утверждать, что полезное значение корректно.
Надеемся, приведённые методы обработки ошибок в Go будут вам полезны. Читайте также статью о 5 главных ошибках Junior-разработчика, чтобы не допускать их в начале своего карьерного пути.
In this article, we shall be discussing how to return and handle errors effectively using custom and inbuilt Golang functions, with help of practical examples.
Golang return error
An error is basically a fault that occurs in a program execution flow. These errors can be of various natures:- Caused by programmers through code syntax and interface errors
, system-related Resources and Runtime errors
, algorithm-related logic and arithmetic errors
. Which later are solved through debugging process.
In Golang ,The Error is an interface that holds Error() string
method. Its implemented as follows
type error interface {
Error() string
}
In an nutshell, when the Error() method is called, it’s return value is in form of string datatype. Through the use of inbuilt Go functions of the fmt and errors packages, we can construct the kind of error message to be displayed. Below is an example to construct Errors using fmt.Error()
in Golang, i.e you want to read a file from a given path, unfortunate the file doesn’t exist or the path given is invalid. For example:=
package main
import (
"fmt"
"os"
)
func ReadFile(file string) error {
dataFile, err := os.ReadFile(file)
if err != nil {
return fmt.Errorf("An error occurred while Reading the file: open : %v", err)
}
fmt.Println(string(dataFile))
return nil
}
func main() {
resultsErr := ReadFile("")
if resultsErr != nil {
fmt.Printf("%v", resultsErr)
}
}
Output:
With the file attached ensure you replace the ReadFile(«test.txt»)
$ go run main.go
Hello
without file attached
$ go run main.go
An error occurred while Reading the file: open: no such file or directory
Explanation:- In the above code, ReadFile() error{}
function returns an error which is nil whenever no error encountered. In Golang, the Error return value is nil as the default, or “zero”. Notice that checking if err != nil{} is the idiomatic way to determine if an error was encountered in Golang syntax, in this function we are returning the error only, handling the file data within the function. fmt.Error()
enables us to customize the kind of message to be displayed. These messages are always in a lowercase format and don’t end with punctuation.
In Golang there are numerous ways to return and handle errors Namely:=
- Casting Errors
- Error wrapping mechanism
- Panic, defer and recover
ALSO READ: How to PROPERLY uninstall Golang? [Step-by-Step]
Different methods of error handling in Go Func
Method 1:- Casting Errors
Casting errors is a way of defining custom and expected errors, with golang we can make use of erros.Isand errors.As() error functions to cast different types of errors. i.e,errors.Is we create a custom error of a particular type and check If the error matches the specific type the function will return true, if not it will return false.
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
var fileNotFound = errors.New("The file doesn't exist")
func ReadFile(file string) error {
dataFile, readErr := os.ReadFile(file)
if readErr != nil {
if errors.Is(readErr, fs.ErrNotExist) {
return fmt.Errorf("this fileName %s doesn't exist ", file)
} else {
return fmt.Errorf("Error occured while opening the file : %w", readErr)
}
}
fmt.Println(string(dataFile))
return nil
}
func main() {
fileName := os.Args[1]
if fileName != "" {
resultsError := ReadFile(fileName)
if resultsError != nil {
fmt.Printf("%v", resultsError)
}
} else {
fmt.Println("the file name cant be empty")
}
}
Output:
$ go run main.go "new"
this fileName new doesn't exist
Explanation:- We are using errors.Is(readErr, fs.ErrNotExist) {} to check if the file passed exist, if it doesn’t exist we return custom message as shown above. we can also use the custom error message such as errors.New() to create expected error and handle it as errors.Is(readErr, fileNotFound) {} the return values will be the same.
Method 2:- Error wrapping
Wrapping is a way of using other errors within a function to provide more context and detailed error messages.
fmt.Error()
function enable us to create a wrapped errors with use of %w flag. The %w
flag is used for inspecting and unwrapping errors.
In this subtitles we can incorporate other functions from errors
package used to handle errors, namely:- errors.As, errors.Is, errors.Unwrap
functions. errors.As
is used to cast a specific error type, i.e func As(err error, target any) bool{}
, also, the errors.Unwrap
is used to inspect and expose the underlying errors in a program,i.e func (e *PathError)Unwrap()error{ return e.Err}
, Furthermore the errors.Is
mostly for comparing error value against the sentinel value if is true or false, i.e func Is(err,target error) bool{}
.
Example of Error Wrapping
package main
import (
"errors"
"fmt"
"os"
)
func ReadFile(file string) error {
dataFile, readErr := os.ReadFile(file)
var pathError *os.PathError
if readErr != nil {
if errors.As(readErr, &pathError) {
return fmt.Errorf("this fileName %s doesn't exist and failed opening file at this path %v", file, pathError.Path)
}
return fmt.Errorf("Error occured while opening the file : %w", readErr)
}
fmt.Println(string(dataFile))
return nil
}
func main() {
fileName := os.Args[1]
if fileName != "" {
resultsError := ReadFile(fileName)
if resultsError != nil {
fmt.Printf("%v", resultsError)
}
} else {
fmt.Println("the file name can't be empty")
}
}
Output:
ALSO READ: Golang Bool to String Type Casting [2 Methods]
With the file attached ensure you replace the ReadFile(«test.txt»)
$ go run main.go
Hello
without file attached
$ go run main.go ""
the file name can't be empty
$ go run main.go next.txt
this fileName news.txt doesn't exist and failed opening file at this path news.txt
Explanation:- In the above code we have used fmt.Errorf() functions to format the error message to be displayed and wrapping error using a custom error message with wrap function errors.Is() which checks if the path exists. You can avoid unnecessary error wrapping and handle it once.
Method-3: Using Panic, Defer and Recover
We have covered this topic in detail in a separate article Golang panic handing [capture, defer, recover, log]
Summary
At this point in this article, you have learned various ways to return and handle errors in the Golang function. In Go, Errors are considered to be a very lightweight piece of data that implements the Error interface. Custom errors in Go help in debugging and signaling where the error has occurred from. Error tracing is easy as compared to other programming languages. Golang application development, error handling is very critical and helps one not only during debugging but also to monitor the application behavior. We recommend you to read more about panic, recover and defer mechanism of error handling as well.
References
error-handling in Go
Working with errors in golang
Errors