> Как организовать обработку успешных и неуспешных сетевых запросов (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 User
if 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() при долгих операциях - утечка горутин и зависшие запросы

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью