Пустой ответ, 503, обрыв и повреждённый JSON
Содержание курса
Таймаут и пустой результат
Контекст ограничивает время ожидания запроса независимо от client.Timeout. В клиентском файле из предыдущего урока он создаётся перед вызовом FetchArticles и передаётся в http.NewRequestWithContext:
client := &http.Client{Timeout: 3 * time.Second}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
articles, err := FetchArticles(ctx, client, baseURL)
Здесь действуют два независимых ограничения: контекст с двухсекундным сроком и client.Timeout в три секунды. http.Client уважает оба и прекращает запрос, как только истечёт любое из них. При обычном зависшем сервере первым сработает контекст — через две секунды.
Важно понимать, когда именно проявляется отмена контекста. Если Do ещё не получил заголовки ответа, он вернёт ошибку напрямую — (nil, err). Если же заголовки уже пришли и Do вернул resp, отмена проявится позже: при чтении resp.Body в json.Decoder.Decode. Ошибка чтения тела из-за отмены — отдельный случай, а не повреждённый JSON из четвёртой строки таблицы, даже если она возникает при Decode. Ни в том ни в другом случае сервер не отправлял статус 503; он мог вообще ничего не ответить. Чтобы программно проверить причину отмены, применяйте errors.Is к err из FetchArticles: эта функция сохраняет исходную причину ошибки. probe возвращает только строку, поэтому его результат для такой проверки не подходит:
errors.Is(err, context.DeadlineExceeded) // истёк срок контекста
errors.Is(err, context.Canceled) // контекст отменён явно
errors.Is работает и с исходной ошибкой. Если функция добавляет к ней пояснение, она должна сохранить причину через %w, чтобы вызывающий код мог её проверить. Угадывать причину по тексту сообщения ненадёжно: формулировки могут меняться между версиями Go.
Теперь о главной ловушке. Соблазн привести все сбои к одному виду — пустому срезу — понятен: меньше ветвлений, код короче. Вот опасная версия:
articles, err := FetchArticles(ctx, client, baseURL)
if err != nil {
articles = []Article{} // сбой теперь выглядит как ответ 200 []
}
fmt.Println("статей:", len(articles))
Она напечатает статей: 0 одинаково при настоящем 200 [], при 503 и при обрыве связи. Вызывающий код теряет информацию о том, что произошло. Пустой срез в Go — корректное значение, а не сигнал об ошибке; подменяя им ошибку, вы делаете её невидимой.
Правильная форма сохраняет оба значения раздельно:
articles, err := FetchArticles(ctx, client, baseURL)
if err != nil {
fmt.Println("не удалось получить статьи:", err)
return
}
fmt.Println("статей:", len(articles))
Эта проверка не решает, повторять ли запрос или как реагировать на сбой в масштабе системы. Она делает ровно одно: точно называет то, что клиент наблюдал при одном обращении к API статей. Именно это различие — между «сервер сказал, что статей нет» и «мы вообще не получили ответ» — и нужно сохранить для любого кода, который будет работать с результатом.
