> Как организовать обработку успешных и неуспешных сетевых запросов (Go)
Уровень: senior · Роль: backend · Категория: Технические вопросы
Компании: Домопульт
Стек: Go
> Пример ответа
Короткий ответ
Обработка сетевых запросов в Go строится вокруг явной проверки ошибок, возвращаемых функциями. Успешный путь - это нормальное выполнение без error, неуспешный - ветка с обработкой err. Для HTTP-серверов важно различать ошибки транспорта (не удалось прочитать запрос) и бизнес-ошибки (валидация, авторизация), которые возвращаются как HTTP-статусы. Ключевой принцип: не игнорировать ошибки, логировать их с контекстом и возвращать клиенту понятный ответ.
Подробное объяснение
В Go нет исключений - ошибки передаются как значения. Это заставляет разработчика явно обрабатывать каждый неуспешный сценарий. Для сетевых запросов есть два уровня обработки:
Уровень транспорта - ошибки соединения, таймауты, закрытие соединения. Они возникают при вызове http.Client.Do, net.Dial, чтении тела запроса. Здесь нужно решать: retry, fallback или возврат ошибки наверх.
Уровень приложения - ошибки бизнес-логики: невалидные данные, отсутствие ресурса, недостаточно прав. Для HTTP-сервера это выражается в HTTP-статусах: 400, 404, 403, 500. В Go принято использовать middleware для централизованной обработки таких ошибок - единая точка, где ошибка преобразуется в JSON-ответ с нужным статусом.
Для клиентской стороны важно различать ошибки сети (можно повторить) и ошибки HTTP-статуса (повторять бессмысленно). В стандартной библиотеке http.Client не возвращает ошибку на 404 - нужно проверять resp.StatusCode самостоятельно.
На практике
Для сервера типичный паттерн - обёртка handler'а, которая ловит панику и ошибку, логирует и возвращает структурированный ответ. Для клиента - использование errors.Is для проверки на context.DeadlineExceeded или io.EOF, чтобы решить, стоит ли retry.
Важно: не логировать ошибку в каждом слое - это создаёт шум. Логируй один раз на границе системы (middleware или на уровне вызывающего кода). Для клиента используй errors.Join или wrapping через fmt.Errorf("...: %w", err), чтобы сохранить цепочку ошибок.
Для таймаутов и отмены контекста - всегда проверяй ctx.Err() в долгих операциях. Это часть обработки неуспешных запросов, так как клиент может отменить запрос.
Пример кода
GO// Сервер: middleware для обработки ошибокfunc errorMiddleware(next http.Handler) http.Handler {return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {defer func() {if rec := recover(); rec != nil {log.Printf("panic: %v", rec)writeError(w, http.StatusInternalServerError, "internal error")}}()next.ServeHTTP(w, r)})}func writeError(w http.ResponseWriter, status int, msg string) {w.Header().Set("Content-Type", "application/json")w.WriteHeader(status)json.NewEncoder(w).Encode(map[string]string{"error": msg})}// Handler с явной обработкой ошибокfunc getUserHandler(w http.ResponseWriter, r *http.Request) {id := r.PathValue("id")user, err := store.GetUser(r.Context(), id)if err != nil {if errors.Is(err, store.ErrNotFound) {writeError(w, http.StatusNotFound, "user not found")return}log.Printf("get user %s: %v", id, err)writeError(w, http.StatusInternalServerError, "internal error")return}json.NewEncoder(w).Encode(user)}// Клиент: различение ошибок сети и HTTP-статусаfunc fetchUser(ctx context.Context, id string) (*User, error) {req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.example.com/users/"+id, nil)resp, err := http.DefaultClient.Do(req)if err != nil {return nil, fmt.Errorf("request failed: %w", err)}defer resp.Body.Close()if resp.StatusCode != http.StatusOK {return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)}var user Userif err := json.NewDecoder(resp.Body).Decode(&user); err != nil {return nil, fmt.Errorf("decode response: %w", err)}return &user, nil}
Как отвечать на собеседовании
Начни с принципа явной обработки ошибок в Go - это фундамент. Затем раздели транспортные и бизнес-ошибки, покажи понимание middleware для сервера и проверки StatusCode для клиента. Упомяни wrapping ошибок через %w и errors.Is для проверки конкретных случаев. Если спросят про retry - скажи, что retry уместен только для транспортных ошибок и идемпотентных запросов, с экспоненциальной задержкой и jitter.
Что проверяет интервьюер
- Понимание идиоматичного подхода Go к ошибкам (без исключений)
- Умение различать типы ошибок и выбирать стратегию для каждой
- Знание паттернов middleware и централизованного логирования
- Понимание разницы между ошибкой сети и ошибкой HTTP-статуса на клиенте
- Внимание к контексту и таймаутам
Типичные ошибки
- Игнорирование ошибки через
_- сразу красный флаг - Логирование ошибки на каждом уровне - дублирование и шум
- Возврат 500 на любую ошибку без разбора - теряется диагностика
- Проверка только
err != nilбез анализа типа ошибки - невозможно решить, нужен ли retry - Отсутствие обработки
ctx.Err()при долгих операциях - утечка горутин и зависшие запросы
> Похожие задачи по backend
Что такое достижимая и недостижимая ссылка в сборщике мусора
Что такое garbage collector и как он работает
Как реализована аутентификация с поддержкой OpenID и OAuth 2.0
Может ли приложение работать в нескольких процессах?
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью