Claude API в Go: запросы через net/http с обработкой SSE

Клиент для Claude и GPT на стандартной библиотеке Go: обычный запрос, разбор ошибок, потоковый ответ через SSE и параллельные запросы с ограничением.

3 мин · · разработка

Если бэкенд написан на Go, официальный SDK для работы с Claude API не обязателен: хватает пакетов net/http и encoding/json из стандартной библиотеки. Ниже — рабочие примеры для Zukko: обычный запрос, разбор ошибок, потоковый ответ (SSE) и параллельные запросы через горутины. Всё идёт через OpenAI-совместимый эндпоинт, поэтому тот же код работает и с Claude, и с GPT, и с GLM — меняется только поле model.

Базовый запрос к Claude API на Go

Ключ берём из переменной окружения ZUKKO_API_KEY, адрес — https://api.zukko.pro/v1/chat/completions.

bash
package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"time"
)

context.WithTimeout ограничивает весь запрос: если ответ не пришёл за 60 секунд, client.Do вернёт ошибку context deadline exceeded, и зависший запрос не заблокирует обработчик. Для длинных ответов увеличьте таймаут. Поля usage.prompt_tokens и usage.completion_tokens — это то, за что вы платите; их удобно писать в лог.

Обработка ошибок

Шлюз возвращает понятные коды, и от кода зависит, есть ли смысл повторять запрос. Удобно завести свой тип ошибки:

bash
type APIError struct {
	Status int
	Body   string
}

func (e *APIError) Error() string {
	switch e.Status {
	case 401:
		return "неверный API-ключ (401)"
	case 402:
		return "недостаточно средств на балансе (402)"
	case 403:
		return "модель не разрешена для этого ключа (403)"
	case 413:

В функции ask замените fmt.Errorf("API %d: %s", ...) на &APIError{Status: resp.StatusCode, Body: string(data)}, а снаружи проверяйте тип через errors.As. Код 403 означает, что модель не входит в список разрешённых для ключа: список задаётся при создании ключа в кабинете. Тело ошибки приходит в формате {"error": {"message": ..., "code": ...}}.

Стриминг ответа через SSE

При "stream": true сервер присылает ответ кусками в формате Server-Sent Events: строки вида data: {...}, разделённые пустыми строками, в конце — data: [DONE]. Последний чанк может прийти со статистикой usage и пустым массивом choices, поэтому длину массива нужно проверять.

bash
package main

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strings"
	"time"

Частая ошибка — сравнивать всю строку с [DONE]: на самом деле приходит data: [DONE], поэтому сначала отрезаем префикс. Буфер сканера увеличен до 1 МБ, чтобы длинное событие не оборвало чтение с ошибкой token too long.

Параллельные запросы через горутины

Go удобен для пакетной обработки: каждая задача — отдельная горутина. Но запускать сотню запросов разом не стоит — получите 429. Ограничьте число одновременных запросов семафором на буферизированном канале. Замените main из первого примера на этот код и добавьте "sync" в import:

bash
func main() {
	apiKey := os.Getenv("ZUKKO_API_KEY")
	client := &http.Client{Timeout: 90 * time.Second}
	ctx := context.Background()

	prompts := []string{
		"Что такое горутина?",
		"Что такое канал в Go?",
		"Чем буферизированный канал отличается от небуферизированного?",
	}

	results := make([]string, len(prompts)) // порядок ответов совпадает с порядком вопросов
	sem := make(chan struct{}, 3)            // не больше 3 запросов одновременно
	var wg sync.WaitGroup

Каждая горутина пишет в свой элемент среза, поэтому мьютекс не нужен, а результаты не перемешиваются. Размер семафора подбирайте опытным путём: начните с 3–5 и увеличивайте, пока не появятся ответы 429.

Стоимость запроса из заголовков ответа

В обычных (не потоковых) ответах шлюз добавляет заголовок X-Zukko-Cost-Micros — стоимость запроса в микродолларах (1 000 000 = $1) — и X-Fetch-Balance-Cents с остатком баланса. Подробно — в разделе о лимитах ключей и кэше. Внутри ask после проверки статуса (нужен импорт strconv):

bash
if micros, err := strconv.ParseInt(resp.Header.Get("X-Zukko-Cost-Micros"), 10, 64); err == nil {
	log.Printf("стоимость запроса: $%.6f", float64(micros)/1e6)
}

Так сервис может сам считать расход и останавливать пакетную обработку до того, как баланс закончится.

Частые вопросы

Где взять API-ключ?

Создайте ключ в кабинете Zukko и сохраните его в переменную окружения ZUKKO_API_KEY. Полный ключ показывается один раз — при создании. Для каждого сервиса удобно заводить отдельный ключ с дневным лимитом.

Почему 404 на /v1/chat/completions?

Проверьте адрес: полный путь — https://api.zukko.pro/v1/chat/completions. Если вы собираете URL из базового адреса, база для OpenAI-формата — https://api.zukko.pro/v1. Справка по формату запроса — в документации Chat Completions.

Как ограничить длину ответа?

Полем max_tokens в теле запроса. Модель остановится на этом лимите, даже если не закончила мысль, — в таком случае finish_reason в ответе будет length.

Какие модели можно указать?

Любую из доступных: claude-sonnet-5, claude-opus-5-5, gpt-5.6-sol, gpt-5.6-luna, glm-5.3, glm-5.3-flash. Актуальный список — GET https://api.zukko.pro/v1/models или страница «Модели» в кабинете. Какая модель лучше подходит для вашей задачи, проверьте на своих данных: прогоните 20–30 типовых запросов через две-три модели и сравните результат и стоимость.

zukko

Платите в 25×
меньше

за Claude Code & ChatGPT

Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.