Хочешь уверенно проходить IT-интервью?

Мы понимаем, как сложно подготовиться: стресс, алгоритмы, вопросы, от которых голова идёт кругом. Но с AI тренажёром всё гораздо проще.
💡 Почему Т1 тренажёр — это мастхэв?
- Получишь настоящую обратную связь: где затык, что подтянуть и как стать лучше
- Научишься не только решать задачи, но и объяснять своё решение так, чтобы интервьюер сказал: "Вау!".
- Освоишь все этапы собеседования, от вопросов по алгоритмам до диалога о твоих целях.
Зачем листать миллион туториалов? Просто зайди в Т1 тренажёр, потренируйся и уверенно удиви интервьюеров. Мы не обещаем лёгкой прогулки, но обещаем, что будешь готов!
Реклама. ООО «Смарт Гико», ИНН 7743264341. Erid 2VtzqwP8vqy
Структура ошибок в 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. Следование им поможет улучшить читаемость кода и обеспечить его единообразие.
Базовое правило именования ошибок заключается в том, что 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
, в программе происходит следующее:
- Останавливается выполнение
func
. - Вызываются все её внутренние defer-функции.
- Запускаются defer-функции, связанные с
func
вплоть до верхнего уровня в исполняемой горутине. - Программа заканчивает выполнение и выводит ошибку, включая значение аргумента panic.
Паника тесно связана с важной сущностью Go – горутинами. Они представляют собой независимые функции, выполняющиеся конкурентно в одном и том же адресном пространстве, являются аналогом корутин в других языках. Горутины имеют динамический стек и управляются рантаймом Go. Главной горутиной является функция main, её завершение приводит к окончанию работы всей программы.
Более подробно горутины будут рассмотрены в последующих частях, в контексте текущей статьи достаточно лишь общего понимания. Для полноценного погружения в тему рекомендуем прочитать материал «Горутины: что такое и как работают».
Recovery
В условиях промышленной разработки часто возникает необходимость обработать панику и вернуть приложение к нормальному выполнению, предотвратив его внезапное завершение. Для этих целей используется механизм восстановления (recovery), реализующийся при помощи встроенной функции recover. Она позволяет восстановить контроль над паникующей горутиной и может быть вызвана только внутри defer-функций.
Recover используется в тех случаях, когда паника не должна привести к завершению всей программы. Например, ошибка в одном из клиентских подключений веб-сервера не должна привести к сбою всего серверного приложения. С помощью recover также можно обрабатывать ошибки в стеке рекурсивных функций и логировать возникшие в программе паники.
Функция recover возвращает nil
, когда горутина не паникует или recover не был напрямую вызван в defer-функции. В иных случаях возвращается значение, отличное от nil
. Если какая-либо горутина запаниковала, то вызов recovery досрочно остановит раскручивание стека, вернет аргумент, переданный в panic
, и возобновит дальнейшее выполнение программы.
При работе с recover следует помнить два важных правила:
recover()
используется только внутри defer-функций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), которое выводится на экран.
Логирование
В продолжение темы обработки ошибок изучим эффективный инструмент для их своевременного отслеживания – логирование.
Логирование – это процесс записи информации обо всех событиях, происходящих в программе. Как правило, полученные данные записываются в специальные файлы, называемые логами. Они содержат сообщения об ошибках, предупреждения, текущее состояние программы, а также пользовательские действия и другую важную информацию.
Логирование полезно по многим причинам:
- Отладка и диагностика ошибок. Логи позволяют разработчикам отслеживать и анализировать ошибки, возникающие в процессе выполнения программы. Записанные данные могут помочь в решении проблем и выявлении их причин.
- Мониторинг и анализ производительности. В логах может содержаться информация о времени выполнения различных операций и объеме потребляемых ресурсов. Это позволяет отслеживать производительность приложения и выявлять места, требующие оптимизации.
- Аналитика и метрики. Логи могут использоваться для сбора данных о поведении пользователей для анализа их активности, предпочтений. Такая информация нужна для улучшения пользовательского опыта и оптимизации функциональности.
- Информационная безопасность. Логирование играет важную роль в обеспечении безопасности приложений, позволяя отслеживать несанкционированный доступ, утечки данных и другие потенциальные угрозы.
Далее рассмотрим различные пакеты для логирования в Go: log, logrus и slog.
Стандартный пакет log
Разработчики языка предусмотрели стандартный пакет log для простого логирования информации. Он ограничен в функциональности, так как предоставляет лишь базовый набор инструментов для частых задач.
Рассмотрим основные функции пакета log:
- Print, Println, Printf – работают как аналогичные функции из пакета fmt.
- Panic, Panicf, Panicln – работают как функции
Print
, но после вывода текста вызываютpanic()
. - Fatal, Fatalln, Fatalf – работают как функции Print, но после вывода текста завершают программу путем вызова
os.Exit(1)
. - SetFlags – устанавливает флаги для форматирования. К примеру, флаг
Lmicroseconds
добавит микросекунды ко времени,Lshortfile
укажет короткий путь к файлу, откуда было получено сообщение. - SetOutput – указывает направление вывода логов. Можно указать любой объект, реализующий интерфейс
io.Writer
: файл, буфер, сетевое соединение и так далее. - 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.
В следующей части погрузимся в парадигму обобщенного программирования и изучим дженерики, а в конце закрепим материал на интересных задачах.
Содержание самоучителя
- Особенности и сфера применения Go, установка, настройка
- Ресурсы для изучения Go с нуля
- Организация кода. Пакеты, импорты, модули. Ввод-вывод текста.
- Переменные. Типы данных и их преобразования. Основные операторы
- Условные конструкции if-else и switch-case. Цикл for. Вложенные и бесконечные циклы
- Функции и аргументы. Области видимости. Рекурсия. Defer
- Массивы и слайсы. Append и сopy. Пакет slices
- Строки, руны, байты. Пакет strings. Хеш-таблица (map)
- Структуры и методы. Интерфейсы. Указатели. Основы ООП
- Наследование, абстракция, полиморфизм, инкапсуляция
- Обработка ошибок. Паника. Восстановление. Логирование
- Обобщенное программирование. Дженерики
- Работа с датой и временем. Пакет time
- Интерфейсы ввода-вывода. Буферизация. Работа с файлами. Пакеты io, bufio, os
- Конкурентность. Горутины. Каналы
- Тестирование кода и его виды. Table-driven подход. Параллельные тесты
- Основы сетевого программирования. Стек TCP/IP. Сокеты. Пакет net
- Протокол HTTP. Создание HTTP-сервера и клиента. Пакет net/http
Комментарии