От Go API к вызову другой программы

Пустой ответ, 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 статей. Именно это различие — между «сервер сказал, что статей нет» и «мы вообще не получили ответ» — и нужно сохранить для любого кода, который будет работать с результатом.

Начать заново?

Помощник начнёт новый диалог по этой задаче. Текущая переписка останется в истории до конца срока хранения.

О переписке

Можно приложить до трёх фото PNG, JPEG или WebP по 5 МБ.

Переписка и фото хранятся 90 дней после последнего принятого сообщения. Преподаватель получит доступ к диалогу, когда вы попросите его помочь.

Проверка решения остаётся отдельным действием.