Интеграции с внешними API в Go
Внешний API - это код, которым вы не управляете. Он может быть медленным, временно недоступным, изменить формат ответа, вернуть неожиданный статус, ограничить количество запросов или прислать ошибку в нестандартном JSON.
Поэтому хорошая интеграция не просто "делает HTTP-запрос". Она изолирует риски: хранит ключи безопасно, ставит таймауты, проверяет статус-коды, обрабатывает rate limits, логирует без секретов и отделяет внешний формат от внутренней бизнес-логики.
Конфигурация и API-ключи
API-ключи храните в переменных окружения:
apiKey := os.Getenv("NEWS_API_KEY") if apiKey == "" { return errors.New("NEWS_API_KEY is required") }
Не записывайте ключи прямо в код:
const apiKey = "real-secret-key" // плохо
Такой ключ легко случайно закоммитить. Если секрет попал в Git, его нужно ротировать, а не только удалить из текущего файла.
Не вставляйте ключи в логи. Если нужно залогировать URL, удалите секретные query parameters:
slog.Info("request external api", "url", redactedURL)
HTTP-клиент
У HTTP-клиента должен быть timeout:
client := &http.Client{ Timeout: 10 * time.Second, }
Каждый запрос должен иметь:
- timeout;
- обработку сетевой ошибки;
- проверку статус-кода;
- лимит размера ответа;
- понятную ошибку;
- retry только там, где это безопасно.
Пример:
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, fmt.Errorf("build request: %w", err) } req.Header.Set("X-Api-Key", apiKey) resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("send request: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected status: %s", resp.Status) }
Авторизация
Внешние API используют разные схемы:
- API key в заголовке;
- Bearer token;
- OAuth2;
- query parameter с ключом;
- signed request.
Пример Bearer:
req.Header.Set("Authorization", "Bearer "+token)
Пример API key:
req.Header.Set("X-Api-Key", apiKey)
Если документация API предлагает передавать ключ в query parameter, будьте осторожны с логами: URL часто сохраняются целиком.
Rate limits
Rate limit означает, что API разрешает только ограниченное количество запросов за период. При превышении часто возвращается:
429 Too Many Requests
Иногда в заголовках есть подсказки:
Retry-After: 30 X-RateLimit-Remaining: 0
Хорошая интеграция не долбит API бесконечно. Она уважает лимиты, использует кэш, backoff и понятную ошибку пользователю.
Retry
Повторные попытки нужны только для временных ошибок:
- timeout;
502 Bad Gateway;503 Service Unavailable;504 Gateway Timeout;- некоторые сетевые сбои.
Не стоит retry-ить:
400 Bad Request;401 Unauthorized;403 Forbidden;- валидационные ошибки;
- операции, которые могут создать дубликат без idempotency key.
Retry должен иметь лимит и backoff:
delay := 200 * time.Millisecond for attempt := 1; attempt <= 3; attempt++ { err := callExternalAPI(ctx) if err == nil { return nil } select { case <-ctx.Done(): return ctx.Err() case <-time.After(delay): delay *= 2 } }
Изолируйте интеграцию
Не размазывайте детали внешнего API по всему проекту. Сделайте отдельный клиент:
type WeatherClient interface { CurrentWeather(ctx context.Context, city string) (Weather, error) }
Сервис работает с интерфейсом, а конкретная реализация знает URL, headers и формат ответа.
Так бизнес-логику проще тестировать: в тесте можно подставить тестовый клиент.
Telegram Bot API
Для Telegram-бота важны:
- токен бота из переменной окружения;
- команды
/start,/help; - обработка текстовых сообщений;
- экранирование MarkdownV2, если используете форматирование;
- graceful shutdown;
- отделение Telegram-слоя от бизнес-логики.
Команда /start должна дать пользователю понятный первый шаг. Ошибки отправки сообщения нужно логировать, но не печатать token.
Open-Meteo и погодные API
Погодные API часто требуют два шага:
- Геокодинг города в координаты.
- Прогноз по координатам.
Кэшируйте геокодинг: координаты города редко меняются. Также нормализуйте название города, потому что Almaty, almaty и Алматы должны вести себя предсказуемо.
NewsAPI и контентные API
Для новостей важно:
- нормализовать дату;
- дедуплицировать статьи;
- хранить источник;
- не доверять заголовкам как уникальному ID;
- обрабатывать rate limits;
- учитывать, что контент может быть пустым или обрезанным.
Контентные API часто возвращают данные разного качества. Код должен быть готов к пустым полям.
Gemini и AI API
Для AI API добавьте:
- таймаут;
- ограничение размера prompt;
- логирование модели без API-ключа;
- retry только при временных ошибках;
- fallback или понятную ошибку пользователю;
- учет стоимости и лимитов, если API платный.
OAuth2
OAuth2 нужен, когда пользователь дает доступ к своему аккаунту: Google Drive, Yandex Disk и другие сервисы.
Обычно flow такой:
- Перенаправить пользователя на страницу провайдера.
- Получить
codeна callback endpoint. - Обменять
codeна access token. - Сохранить refresh token безопасно.
- Обновлять access token по необходимости.
OAuth2 сложнее API key, потому что token принадлежит конкретному пользователю. Его нужно хранить безопасно и уметь обновлять.
Чеклист
- Ключи приходят из env.
- HTTP-клиент имеет timeout.
- Все запросы принимают
context.Context. - Статус-коды проверяются явно.
- Rate limits обрабатываются.
- Retry ограничены и применяются только к временным ошибкам.
- Секреты не пишутся в логи.
- Интеграция отделена интерфейсом от бизнес-логики.
- Внешний JSON преобразуется во внутренние типы.
- Для OAuth2 refresh token хранится безопасно.