Share This
Связаться со мной
Крути в низ
Categories
//Go для начинающих. Обработка ошибок. Паника. Восстановление. Логирование

Go для начинающих. Обработка ошибок. Паника. Восстановление. Логирование

go dlja nachinajushhih obrabotka oshibok panika vosstanovlenie logirovanie e1035d9 - Go для начинающих. Обработка ошибок. Паника. Восстановление. Логирование

Энтузиаст-разработчик, автор статей по программированию. Сфера интересов — backend, web 3.0, кибербезопасность. Рассмотрим устройство механизма ошибок в Go и методы их обработки, познакомимся с функциями паники и восстановления, а также научимся логировать информацию о состоянии программы с помощью различных логеров.

go dlja nachinajushhih obrabotka oshibok panika vosstanovlenie logirovanie 033308e - Go для начинающих. Обработка ошибок. Паника. Восстановление. Логирование

Часть 10 ← Введение в ООП. Наследование, абстракция, полиморфизм, инкапсуляция.

Структура ошибок в Go

В отличие от популярных языков, таких как JavaScript, Python, С++, в Go принято взаимодействовать с ошибками через отдельное возвращаемое значение типа error. При этом нулевое значение (nil) говорит о том, что ошибки не возникло. Такой подход позволяет наглядно выделить функции, возвращающие ошибки, и обрабатывать их с использованием обычных синтаксических конструкций.

Тип error представляет собой интерфейс с единственным методом Error, возвращающим текстовое описание ошибки:

         type error interface {     Error() string }      

Идиоматическим способом обработки ошибок является проверка возвращаемого значения с помощью условного оператора if. Проиллюстрируем это на примере функции strconv.Atoi из пакета strconv со следующей сигнатурой: func Atoi(s string) (int, error) Можем видеть, что она возвращает два значения – результат преобразования строки в число и ошибку:

         res, err := strconv.Atoi("123a") if err != nil { 	fmt.Println(err) 	return } fmt.Println(res)      

Так как мы передали строку, состоящую не только из цифр, то в результате выполнения кода значение err будет отлично от nil, и в консоль будет выведено описание возникшей ошибки: strconv.Atoi: parsing "123a": invalid syntax.

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

Для создания ошибок с произвольным текстом используются функции errors.New() и fmt.Errorf():

         // errors.New() принимает текстовое описание ошибки if err != nil { 	return errors.New("описание ошибки") }  // fmt.Errorf() позволяет передать параметры в описание ошибки param := 0 if err != nil { 	return fmt.Errorf("описание ошибки c параметром типа int: %d", param) }      

Часто возникает необходимость создания индивидуальных типов ошибок для обработки конкретных сбоев в программе. Для этого нужно реализовать у этих типов интерфейс error посредством создания метода Error():

         type errorConst string // тип константной ошибки  const ErrConst errorConst = "stack overflow"  func (e errorConst) Error() string { 	return string(e) }      

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

В качестве иллюстрации этого метода обработки можно привести тип ParseError из пакета net стандартной библиотеки:

         // ParseError — это тип ошибки синтаксического анализа сетевых адресов. type ParseError struct { 	// Type — это тип ожидаемой строки, например 	// "IP address", "CIDR address". 	Type string  	// Text — это неверная текстовая строка. 	Text string }  func (e *ParseError) Error() string { 	return "invalid " + e.Type + ": " + e.Text  }      

Интерфейс error требует реализации единственного метода Error(), но для отдельных случаев бывает полезно определить дополнительные. К примеру, в том же пакете net стандартной библиотеки некоторые реализации ошибок имеют вспомогательные методы, определенные интерфейсом net.Error:

         package net  type Error interface {     error     Timeout() bool   // временные ошибки     Temporary() bool // постоянные ошибки }      

В подобных ситуациях для обработки ошибки можно использовать механизм type assertion, который позволит определить её тип. Проиллюстрируем это на конкретном блоке кода, который проверяет постоянной или временной является возникшая ошибка подключения. В первом случае установим задержку, после которой продолжим выполнение, а во втором – выведем ошибку и завершим программу:

         // Механизм type assertion для приведения ошибки к типу net.Error if netErr, ok := err.(net.Error); ok && netErr.Temporary() {     time.Sleep(1e9)     continue }  // Если ошибка временная (Timeout), то выводим её и завершаем программу if err != nil {     fmt.Println(err)     os.Exit(1) }      

👨‍💻 Библиотека Go разработчика Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека Go разработчика» 🎓 Библиотека Go для собеса Подтянуть свои знания по Go вы можете на нашем телеграм-канале «Библиотека Go для собеса» 🧩 Библиотека задач по Go Интересные задачи по Go для практики можно найти на нашем телеграм-канале «Библиотека задач по Go»

Правила именования ошибок

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

Базовое правило именования ошибок заключается в том, что error-переменные начинаются с err или Err, а error-типы заканчиваются на Error:

         var ErrProhibitedBehavior = errors.New("packagename: prohibited behaviour")  type SomeError struct { 	Line, Column int }      

Также стоит запомнить, что строка описания ошибки не должна начинаться с большой буквы.

В описании ошибок следует указывать их примерное местоположение: название пакета, функции или метода. К примеру, в пакете expression текстовое описание ошибки валидации математического выражения может выглядеть так: “expression: invalid format”.

Panic

Panic (паника) – это встроенная функция, которая останавливает поток выполнения программы и запускает механизм паники. Он похож на исключения в C++ и Java, и может быть вызван ошибками runtime, такими как выход за границы массива, деление на ноль, а также напрямую с помощью ключевого слова panic.

Рассмотрим механизм паники на конкретном примере: создадим функцию DivideNums для деления вещественных чисел. Если делитель равен нулю, то вызовем panic с текстом "Division by zero", иначе – вернем результат деления:

         func DivideNums(a, b float32) float32 { 	if b == 0.0 { 		panic("Division by zero") 	} 	return a / b }  func main() { 	fmt.Println(DivideNums(1, 0)) // panic 	fmt.Println(DivideNums(1, 2)) // Этот код не выполнится }      

В случае вызова функции с аргументами 1 и 0 возникнет паника, на экран будет выведено примерно следующее:

         panic: Division by zero  goroutine 1 [running]: main.DivideNums(...)         /path/to/folder/main.go:7 main.main()         /path/to/folder/main.go.go:13 +0x25 exit status 2      

Разберем процесс паники подробнее. Когда некоторая функция func вызывает panic, в программе происходит следующее:

  1. Останавливается выполнение func.
  2. Вызываются все её внутренние defer-функции.
  3. Запускаются defer-функции, связанные с func вплоть до верхнего уровня в исполняемой горутине.
  4. Программа заканчивает выполнение и выводит ошибку, включая значение аргумента panic.

Паника тесно связана с важной сущностью Go – горутинами. Они представляют собой независимые функции, выполняющиеся конкурентно в одном и том же адресном пространстве, являются аналогом корутин в других языках. Горутины имеют динамический стек и управляются рантаймом Go. Главной горутиной является функция main, её завершение приводит к окончанию работы всей программы.

Более подробно горутины будут рассмотрены в последующих частях, в контексте текущей статьи достаточно лишь общего понимания. Для полноценного погружения в тему рекомендуем прочитать материал «Горутины: что такое и как работают».

Recovery

В условиях промышленной разработки часто возникает необходимость обработать панику и вернуть приложение к нормальному выполнению, предотвратив его внезапное завершение. Для этих целей используется механизм восстановления (recovery), реализующийся при помощи встроенной функции recover. Она позволяет восстановить контроль над паникующей горутиной и может быть вызвана только внутри defer-функций.

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

Функция recover возвращает nil, когда горутина не паникует или recover не был напрямую вызван в defer-функции. В иных случаях возвращается значение, отличное от nil. Если какая-либо горутина запаниковала, то вызов recovery досрочно остановит раскручивание стека, вернет аргумент, переданный в panic, и возобновит дальнейшее выполнение программы.

При работе с recover следует помнить два важных правила:

  1. recover() используется только внутри defer-функций
  2. recover() работает только в той горутине, где была вызвана паника

Рассмотрим применение recover на предыдущем примере с функцией DivideNums. На этот раз запустим механизм восстановления после возникновения паники “Division by zero”:

         func DivideNums(a, b float32) float32 { 	defer func() { 		if r := recover(); r != nil { 			fmt.Println("Recovery:", r) 		} 	}()  	if b == 0.0 { 		panic("Division by zero") 	}  	return a / b }  func main() { 	fmt.Println(DivideNums(10, 0)) 	fmt.Println(DivideNums(10, 5)) }      

После выполнения кода получим следующий вывод:

         Recovery: Division by zero 0 2      

Давайте детально разберем работу написанной программы. При передаче аргументов (10, 0) в функцию DivideNums возникает паника, которая запускает отложенный вызов defer. Он проверяет возникновение паники с помощью recover(): если значение не равно nil, то паника обрабатывается, на экран выводится сообщение “Recovery: Division by zero”, после чего программа возвращается к нормальному выполнению. Поскольку произошла паника, функция DivideNums() завершается, возвращая значение по умолчанию для float32, равное 0. Далее следует вызов DivideNums с двумя ненулевыми числами, поэтому в результате возвращается ожидаемое значение (2), которое выводится на экран.

Логирование

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

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

Логирование полезно по многим причинам:

  1. Отладка и диагностика ошибок. Логи позволяют разработчикам отслеживать и анализировать ошибки, возникающие в процессе выполнения программы. Записанные данные могут помочь в решении проблем и выявлении их причин.
  2. Мониторинг и анализ производительности. В логах может содержаться информация о времени выполнения различных операций и объеме потребляемых ресурсов. Это позволяет отслеживать производительность приложения и выявлять места, требующие оптимизации.
  3. Аналитика и метрики. Логи могут использоваться для сбора данных о поведении пользователей для анализа их активности, предпочтений. Такая информация нужна для улучшения пользовательского опыта и оптимизации функциональности.
  4. Информационная безопасность. Логирование играет важную роль в обеспечении безопасности приложений, позволяя отслеживать несанкционированный доступ, утечки данных и другие потенциальные угрозы.

Далее рассмотрим различные пакеты для логирования в Go: log, logrus и slog.

Стандартный пакет log

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

Рассмотрим основные функции пакета log:

  1. Print, Println, Printf – работают как аналогичные функции из пакета fmt.
  2. Panic, Panicf, Panicln – работают как функции Print, но после вывода текста вызывают panic().
  3. Fatal, Fatalln, Fatalf – работают как функции Print, но после вывода текста завершают программу путем вызова os.Exit(1).
  4. SetFlags – устанавливает флаги для форматирования. К примеру, флаг Lmicroseconds добавит микросекунды ко времени, Lshortfile укажет короткий путь к файлу, откуда было получено сообщение.
  5. SetOutput – указывает направление вывода логов. Можно указать любой объект, реализующий интерфейс io.Writer: файл, буфер, сетевое соединение и так далее.
  6. SetPrefix – устанавливает префикс для логов.

Продемонстрируем применение этих функций в коде:

         func main() { 	log.Print("сообщение от log.Print")  	log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile) 	log.Println("сообщение с флагами")  	var buf bytes.Buffer // объявление буфера 	bufLogger := log.New(&buf, "buf:", log.LstdFlags) 	bufLogger.Print("сообщение в буфере buf")  	bufLogger.SetPrefix("bufPrefix:") 	bufLogger.Print("сообщение в буфере buf с префиксом bufPrefix:") 	fmt.Print(buf.String())  	// открытие файла "logfile.log" для логов 	// если файла нет, то он будет создан 	file, err := os.OpenFile("logfile.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) 	if err == nil { 		log.SetOutput(file) // перенаправление вывода логов в файл logfile.log 	} else { 		log.Panic("Ошибка открытия файла логов:", err) 	} 	defer file.Close() // отложенное закрытие файла }      

В результате выполнения кода получим следующий результат, который будет различаться в зависимости от времени запуска:

         2024/03/14 11:52:16 сообщение от log.Print 2024/03/14 11:52:16.222089 main.go:14: сообщение с флагами buf:2024/03/14 11:52:16 сообщение в буфере buf bufPrefix:2024/03/14 11:52:16 сообщение в буфере buf с префиксом bufPrefix:      

Пакет logrus

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

Logrus предоставляет 7 уровней логирования, упорядоченных по возрастанию значимости в программе: Trace, Debug, Info, Warning, Error, Fatal and Panic. Уровень задается с помощью функции SetLevel. Ниже представлен код, демонстрирующий применение всех уровней логирования и их назначение:

         func main() { 	logrus.SetLevel(logrus.TraceLevel) 	logrus.Trace("Для отслеживания определенной информации") 	logrus.Debug("Информация для отладки") 	logrus.Info("Информация о действиях и состоянии программы") 	logrus.Warn("Информация, требующая внимания") 	logrus.Error("Ошибка, которая не приводит к завершению программы") 	logrus.Fatal("Ошибка, вызывающая завершение работы службы или приложения") 	logrus.Panic("Ошибка, вызывающая panic") }      

В результате будут выведены цветные префиксы уровней логирования с их описанием:

         TRAC[0000] Для отслеживания определенной информации      DEBU[0000] Информация для отладки                        INFO[0000] Информация о действиях и состоянии программы  WARN[0000] Информация, требующая внимания                ERRO[0000] Ошибка, которая не приводит к завершению программы  FATA[0000] Ошибка, вызывающая завершение работы службы или приложения       

Отметим, что если указать logrus.SetLevel(logrus.InfoLevel) вместо logrus.SetLevel(logrus.TraceLevel), то сообщения уровней ниже Info, то есть Trace и Debug, выведены не будут.

Logrus позволяет добавить к данным дополнительное описание с помощью функции logrus.WithFields:

         func main() { 	// логирование запуска TCP-сервера на порту 8080 	logrus.WithFields(logrus.Fields{ 		"network":  "tcp", 		"address:": ":8080", 	}).Info("Starting server...") }      

Приведенный выше код выведет сообщение о старте сервера с указанием двух дополнительных полей: адреса (:8080) и протокола (tcp):

         INFO[0000] Starting server...   address:=":8080" network=tcp      

Для установки формата вывода логов используется функция logrus.SetFormatter

         func main() { 	logrus.SetFormatter(&logrus.JSONFormatter{}) // JSON формат 	logrus.Info("JSONFormatter INFO message") 	 	logrus.SetFormatter(&logrus.TextFormatter{ // настраиваемый текстовый формат 		DisableColors: true, // отключение цветов 		FullTimestamp: true, // формат полной даты 	}) 	logrus.Info("TextFormatter INFO message") }      

В результате выполнения кода на первой строке получим сообщение в JSON формате, а на втором – в текстовом с заданными параметрами:

         {"level":"info","msg":"JSONFormatter INFO message","time":"2024-03-14T17:23:45+03:00"} time="2024-03-14T17:23:45+03:00" level=info msg="TextFormatter INFO message"      

Пакет slog

Пакет slog был предложен сообществом энтузиастов как альтернатива log, после чего поддержан разработчиками и выпущен в версии Go v1.21 по адресу log/slog. Он обладает обратной совместимостью с log и заимствует из него некоторые функции, но обладает расширенным функционалом и возможностью детальной настройки.

В пакете slog присутствует 4 основных уровня логирования, которые идентифицируются целыми числами с интервалом в 4: Debug (-4), Info (0), Warn (4), Error (8). Такой подход предоставляет пользователям возможность добавить свои уровни между четырьмя стандартными. К примеру, можно создать новый уровень логирования между Debug и Info с целыми значениями в интервале (-4;0).

Функции пакета slog схожи с рассмотренными ранее. Для сравнения разберем их на конкретном примере:

         func main() { 	slogger := slog.New(slog.NewTextHandler(os.Stdout, nil)) // создание логера 	slog.SetDefault(slogger) // установка логера по умолчанию  	slogger.Debug("Debug message") 	slogger.Info("Info message") 	slogger.Warn("Warn message") 	slogger.Error("Error message")  	// Сообщение от старого логера из пакета log будет 	// преобразовано в формат нового логера из пакета slog 	log.Print("Message from old logger")  	// Сообщение с атрибутами в виде пар "ключ-значение" 	slogger.Info( 		"Server started", 		"port", ":8080", 		"network", "tcp", 	) }      

В результате выполнения кода в консоль будет выведено следующее:

         time=2024-03-14T18:52:08.437+03:00 level=INFO msg="Info message" time=2024-03-14T18:52:08.437+03:00 level=WARN msg="Warn message" time=2024-03-14T18:52:08.437+03:00 level=ERROR msg="Error message" time=2024-03-14T18:52:08.437+03:00 level=INFO msg="Message from old logger" time=2024-03-14T18:52:08.437+03:00 level=INFO msg="Server started" port=:8080 network=tcp      

Синтаксис чередующегося значения ключей для атрибутов удобен, но для часто выполняемых операторов может быть более эффективным использовать тип Attr с методом LogAttrs. Такой подход позволит оптимизировать потребление памяти и обеспечить безопасность типов при указании дополнительных атрибутов. Учитывая это, заменим предыдущий код с выводом сообщения “Server started” на аналогичный с использованием LogAttrs:

         func main() { 	slogger := slog.New(slog.NewTextHandler(os.Stdout, nil)) // создание логера  	// Сообщение с атрибутами в виде пар "ключ-значение" 	slogger.LogAttrs( 		context.Background(), 		slog.LevelInfo, 		"Server started", 		slog.String("port", ":8080"), 		slog.String("network", "tcp"), 	) }       

В консоль будет выведено следующее сообщение:

         time=2024-03-14T19:39:59.247+03:00 level=INFO msg="Server started" port=:8080 network=tcp      

Настройка обработчиков, включая TextHandler и JSONHandler, производится с помощью типа HandlerOptions:

         func main() { 	opts := &slog.HandlerOptions{ 		AddSource: true,           // указание пути к файлу 		Level:     slog.LevelInfo, // задание минимального уровня 	} 	slogger := slog.New(slog.NewTextHandler(os.Stdout, opts)) // создание логера  	slogger.Debug("Debug message") 	slogger.Info("Info message") }      

После выполнения кода будет выведено только сообщение уровня Info с указанием пути к исполняемому файлу:

         time=2024-03-14T19:24:38.685+03:00 level=INFO source=/home/herman/myfolder/solve/main.go:16 msg="Info message"      

Подведем итоги

В этой части самоучителя мы расширили наше представление об ошибках, узнали методы их обработки, а также познакомились с механизмами паники и восстановления. Во втором блоке статьи было рассмотрено логирование в Go с использованием трех различных пакетов: log, logrus и slog.

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

***

Содержание самоучителя

  1. Особенности и сфера применения Go, установка, настройка
  2. Ресурсы для изучения Go с нуля
  3. Организация кода. Пакеты, импорты, модули. Ввод-вывод текста.
  4. Переменные. Типы данных и их преобразования. Основные операторы
  5. Условные конструкции if-else и switch-case. Цикл for. Вложенные и бесконечные циклы
  6. Функции и аргументы. Области видимости. Рекурсия. Defer
  7. Массивы и слайсы. Append и сopy. Пакет slices
  8. Строки, руны, байты. Пакет strings. Хеш-таблица (map)
  9. Структуры и методы. Интерфейсы. Указатели. Основы ООП
  10. Наследование, абстракция, полиморфизм, инкапсуляция
  11. Обработка ошибок. Паника. Восстановление. Логирование

  • 5 views
  • 0 Comment

Leave a Reply

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

Связаться со мной
Close