Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 5 из 25 ~18 мин +50 XP

Клиенты: подключение из Go

Как вызвать Connect-сервис из Go-кода? Плагин генерирует типизированный клиент: конструктор принимает обычный http.Client и адрес сервера, а вызов метода выглядит как обычная функция. Разберём connect клиент go от и до: три протокола и опции WithGRPC и WithGRPCWeb, требования к HTTP/2, метаданные через connect.NewRequest, таймауты и отмену через контекст, плюс универсальный connect.NewClient для вызовов без сгенерированного кода.

Содержание урока · 12 разделов

В прошлом уроке мы разобрали три протокола на проводе и выяснили главное: один и тот же сгенерированный handler обслуживает и Connect-протокол, и классический gRPC, и gRPC-Web, различая их по Content-Type. Но сервер — это только половина истории: рано или поздно вам понадобится собственный Go-клиент. Сервису заметок понадобится вызывать сервис профилей, воркер — дёргать сервис нотификаций, а CLI-утилита — общаться с вашим API из терминала. Хорошая новость: connect client на стороне Go устроен так же просто, как и сервер, — с ним вы знакомы после урока про первый Connect-сервис. Плагин protoc-gen-connect-go генерирует не только handler-конструктор, но и типизированный клиент с той же сигнатурой методов, что у интерфейса обработчика. Ваша задача — выбрать http.Client, адрес сервера и протокол; всё остальное — кодирование сообщений, маршрутизация по пути, коды ошибок, сжатие — библиотека берёт на себя. Этот урок — connect клиент go в одном месте: сначала базовый вызов, затем выбор протокола и тонкости HTTP/2, потом метаданные, таймауты и отмена, а в финале — универсальный generic-клиент для случаев, когда сгенерированного кода под рукой нет.

Сгенерированный клиент: что даёт плагин#

Для каждого сервиса из .proto-схемы плагин создаёт три вещи. Первая — интерфейс NoteServiceClient с методами, зеркально повторяющими rpc-объявления: GetNote и CreateNote из схемы превращаются в методы с сигнатурой (запрос) — (ответ, error). Вторая — конструктор NewNoteServiceClient(httpClient, baseURL, options...), который собирает реализацию этого интерфейса. Обратите внимание на первый аргумент: это не строка и не специальный канал связи, а самый обыкновенный *http.Client из стандартной библиотеки — тот самый, которым вы ходите в REST-эндпоинты. Третья — интерфейсная проверка на этапе компиляции: если серверная реализация и клиентский интерфейс расходятся со схемой, сборка падает раньше, чем код доедет до прода. Это тот же proto-first контракт, который вы знаете по gRPC: типы генерируются из схемы, а не описываются руками. Конструктор также принимает переменный список опций ClientOption — именно через них чуть позже мы будем выбирать протокол, включать сжатие и подключать перехватчики. Если вы проходили курс по gRPC, сопоставьте это с grpc.NewClient и NewNoteServiceClient из пакета pb: связь с сервером, балансировка и переиспользование соединений там прячутся внутри ClientConn, здесь же роль транспорта играет честный http.Client со всеми его таймаутами, cookie jar-ом и прокси-настройками.

Базовый вызов: четыре строки

GO
package main

import (
        "context"
        "log"
        "net/http"

        notesv1 "example.com/notes/gen/notes/v1"
        notesv1connect "example.com/notes/gen/notes/v1/notesv1connect"
)

func main() {
        // Первый аргумент — обычный *http.Client, второй — адрес сервера.
        client := notesv1connect.NewNoteServiceClient(
                http.DefaultClient,
                "http://localhost:8080",
        )

        // Вызов выглядит как обычная функция: запрос, ответ, ошибка.
        note, err := client.GetNote(context.Background(), &notesv1.GetNoteRequest{Id: "n1"})
        if err != nil {
                log.Fatal(err)
        }
        log.Print(note.Title)
}

Смотрите, сколько здесь нет: нет ручного конструирования URL, нет marshalling в JSON или protobuf, нет разбора кодов ошибок. Конструктор получает http.Client и базовый адрес, метод GetNote — контекст и message-тип запроса, а в ответ приходит message-тип ответа и error. Путь процедуры (/notes.v1.NoteService/GetNote) библиотека выводит из схемы сама — тот же путь, по которому curl стучался в демо-сервер ещё в первом уроке. http.DefaultClient здесь осознанный выбор: для Connect-протокола хватает HTTP/1.1, о чём ниже.

Протокол выбирается опцией, а не новым клиентом#

По умолчанию сгенерированный клиент разговаривает на собственном протоколе Connect — том самом, который едет по HTTP/1.1 или HTTP/2 без трейлеров и с осмысленными HTTP-статусами. Если сервер собран на Connect (или на чём-то, что понимает все три протокола, как наш handler из урока про протоколы), ничего настраивать не нужно. А вот чтобы заставить клиента говорить по-другому, есть две опции. connect.WithGRPC() переключает клиента на классический gRPC — полный паритет со спецификацией, включая трейлеры и требование HTTP/2. connect.WithGRPCWeb() включает gRPC-Web — протокол с закодированными в тело трейлерами, который исторически придумали для браузеров, но иногда используют и в Go. Зачем это нужно на практике? Первая причина — окружение: перед вашим сервисом стоит балансировщик или ingress, который понимает только gRPC, и клиент должен вписаться в эту цепочку. Вторая — совместимость: вы вызываете чужой сервис, собранный на чистом grpc-go, и он принимает только application/grpc. В обоих случаях сгенерированный клиент не меняется — меняется лишь опция в конструкторе. Схема, код вызова, обработка ошибок остаются прежними: это и есть обещание «один клиент — три протокола», зеркальное к серверному «один handler — три протокола».

Протокол и требования к http.Client#

ПротоколОпцияТранспортКогда выбирать
Connect (по умолчанию)не нужнаHTTP/1.1 или HTTP/2, http.DefaultClient подходитсвой Connect-сервер, curl-отладка, максимум простоты
gRPCconnect.WithGRPC()только HTTP/2: TLS-адрес или http2.Transportчистые gRPC-серверы, строгий ingress, паритет со спецификацией
gRPC-Webconnect.WithGRPCWeb()HTTP/1.1 или HTTP/2прокси-цепочки и клиенты, заточенные под gRPC-Web

Нужно, чтобы сгенерированный клиент общался с чистым gRPC-сервером по протоколу gRPC. Что потребуется?

HTTP/2 на практике: TLS и cleartext#

Пункт про HTTP/2 стоит разобрать по-инженерному, потому что именно здесь новички спотыкаются чаще всего. Ситуация первая: сервер за TLS, адрес начинается с https. Стандартный http.Client умеет переговоры ALPN и поднимет HTTP/2 сам — ничего делать не нужно. Ситуация вторая: локальная разработка, сервер слушает открытый TCP без сертификатов, а клиенту нужен именно gRPC-протокол. Обычный http.Client по http:// пойдёт на HTTP/1.1, и gRPC-вызов развалится. Лечится это транспортом h2c — HTTP/2 без шифрования — из пакета golang.org/x/net/http2: создаёте http2.Transport с AllowHTTP: true и DialTLSContext, который вместо рукопожатия TLS просто открывает TCP-соединение, и оборачиваете его в http.Client. Код ниже — канонический рецепт из документации Connect. Третья ситуация: продакшен за ingress. Здесь правило простое — HTTP/2 должен дожить от клиента до сервера целиком; если посередине стоит терминирующий TLS-балансировщик, проверьте, что и следующий хоп тоже HTTP/2 или что он честно переводит запросы, иначе gRPC-протокол обломается посередине пути. Для Connect-протокола и gRPC-Web этих требований нет: они осознанно спроектированы жить и на HTTP/1.1 — ещё одна причина, почему дефолтный протокол такой удобный.

gRPC-клиент на cleartext HTTP/2

GO
// Транспорт HTTP/2 поверх открытого TCP (h2c) для локальной разработки.
transport := &http2.Transport{
        AllowHTTP: true,
        DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
                var d net.Dialer
                return d.DialContext(ctx, network, addr)
        },
}

client := notesv1connect.NewNoteServiceClient(
        &http.Client{Transport: transport},
        "http://localhost:8080",
        // Единственная connect-специфика: говорим по-настоящему gRPC-протоколом.
        connect.WithGRPC(),
)

DialTLSContext звучит странно, но смысл честный: http2.Transport всегда открывает соединение через функцию дозвона, а AllowHTTP разрешает ему не делать TLS-рукопожатие. Дальше обычный http.Client с этим транспортом и опция connect.WithGRPC(). Если сервер за TLS — вся эта конструкция не нужна: DefaultClient с https-адресом поднимет HTTP/2 через ALPN автоматически.

Клиент просит протокол gRPC, но транспорт остался DefaultClient. Чем закончится вызов?

Прочитайте код и выберите, что он выведет:

// Сервер — наш Connect-handler: понимает Connect, gRPC и gRPC-Web.
client := notesv1connect.NewNoteServiceClient(
        http.DefaultClient, // без http2.Transport
        "http://localhost:8080",
        connect.WithGRPC(),
)
note, err := client.GetNote(ctx, &notesv1.GetNoteRequest{Id: "n1"})

Метаданные: заголовки запроса#

Редкий вызов живёт без метаданных: трассировочные заголовки, идентификатор запроса, токен доступа — всё это должно доехать от клиента до handler-а. В gRPC для этого существовали metadata.NewOutgoingContext и md.Pairs — мы подробно разбирали эту механику в уроке про метаданные и контекст курса gRPC. В Connect философия проще: метаданные — это обычные HTTP-заголовки, без промежуточного типа. Для сгенерированного клиента Unary-вызов принимает message напрямую, поэтому вкладывать заголовки в каждый вызов он не умеет — их добавляет перехватчик, о котором поговорим в седьмом уроке: клиентский interceptor навешивает Authorization на каждый исходящий запрос в одном месте. А вот у универсального generic-клиента, к которому мы сейчас переходим, запрос — это полноценный объект connect.NewRequest с методом Header(), и заголовки ставятся прямо на нём. Заметьте симметрию с сервером: handler читает их через req.Header().Get — один и тот же map, одна и та же семантика, никакого нового словаря. Именно поэтому между gRPC и Connect так просто переносятся трассировка и авторизация: стандартные заголовки остаются стандартными заголовками.

Generic-клиент: connect.NewClient и CallUnary

GO
package main

import (
        "context"
        "log"
        "net/http"

        "connectrpc.com/connect"
        notesv1 "example.com/notes/gen/notes/v1"
)

func main() {
        // Generic-клиент: типы запроса и ответа — параметрами, путь — строкой.
        client := connect.NewClient[notesv1.GetNoteRequest, notesv1.Note](
                http.DefaultClient,
                "http://localhost:8080/notes.v1.NoteService/GetNote",
        )

        // Запрос — объект: у него есть Header() для метаданных.
        req := connect.NewRequest(&notesv1.GetNoteRequest{Id: "n1"})
        req.Header().Set("X-Request-Source", "cli")

        res, err := client.CallUnary(context.Background(), req)
        if err != nil {
                log.Fatal(err)
        }
        log.Print(res.Msg.Title)
}

connect.NewClient — универсальный клиент из самого пакета connectrpc.com/connect: вместо сгенерированного интерфейса он параметризуется типами запроса и ответа, а полный путь процедуры задаётся строкой. Метод CallUnary принимает connect.NewRequest и возвращает *connect.Response[Note], где ответ живёт под полем Msg. Зачем он нужен? Во-первых, для сервисов без protobuf-схем или при сырых вызовах. Во-вторых — в тестах и инструментах, где хочется дёргать произвольные процедуры. В-третьих, как здесь: когда нужны заголовки на конкретном вызове.

Когда сгенерированного клиента недостаточно и уместен generic connect.NewClient?

Контекст: таймауты и отмена#

Контекст в Connect — не церемониальный параметр, а полноценный канал управления вызовом, точно так же, как и в gRPC и в обычном net/http. context.WithTimeout ограничивает время жизни вызова: если сервер не успел ответить до истечения дедлайна, клиент возвращает ошибку с кодом deadline_exceeded — и это не локальная имитация, а честный обрыв обеих сторон. Важно понимать, что дедлайн едет по проводу: gRPC передаёт его заголовком grpc-timeout, а у Connect-протокола для unary-вызовов есть собственный заголовок connect-timeout-ms — сервер видит дедлайн и старается уложиться, а если не успевает, возвращает соответствующий код. Отмена работает так же: context.WithCancel плюс cancel() по крестику в интерфейсе — и обе стороны получают уведомление, что продолжать смысла нет; серверный хендлер, который уважает ctx.Done(), перестанет гореть напрасно. Практические привычки из мира net/http здесь полностью переносимы: дедлайн ставьте на каждый вызов, а не только на «долгие»; значения таймаута наследуйте от SLA вышестоящего запроса, а не от потолка сервера; никогда не игнорируйте возвращённый cancel — иначе утечёте горутиной. Если вы привыкли к правилам курса net/http — здесь они без изменений, и это большой подарок по сравнению с собственным тайм-менеджментом gRPC-клиента.

Клиент — это интерфейс: моки и тесты#

Ещё одно следствие proto-first подхода, которое вы оцените ближе к проекту: сгенерированный клиент — это интерфейс, а не конкретный тип. Там, где ваш сервис зависит от NoteServiceClient, вы принимаете интерфейс и спокойно подставляете фейк в тестах — ровно та же дисциплина, которой учит любой зрелый Go-проект. Возьмите за правило: бизнес-логика не собирает http.Client и не знает адресов — конструктор клиента вызывается в main, а вниз по слоям передаётся уже интерфейс. Если хочется границу провести ещё жёстче, определите свой маленький интерфейс потребителя с одним-двумя методами — GetNote, CreateNote — и адаптируйте сгенерированный клиент к нему в композиционном корне; на этапе тестирования подмените его ручной заглушкой, которая возвращает фиксированную заметку. Полноценную механику мок-клиентов, httptest-серверы и приёмы интеграционных тестов мы отдельно разберём в шестнадцатом уроке про тестирование Connect-сервисов — там пригодится и generic-клиент из этого урока, потому что он позволяет строить лёгкие in-memory заглушки без сети. Пока запомните принцип: зависимость от клиента объявляет потребитель, реализация приходит снаружи. Это тот же шов, вокруг которого строились тесты в курсах Gin и Echo, только типы теперь генерируются из схемы.

Сервер вернул not_found на вызов GetNote. Что именно получит клиент в Go?

Соберите базовый вызов клиента

Восстановите конструктор сгенерированного клиента, имя метода и проверку ошибки.

client := notesv1connect.( http.DefaultClient, "http://localhost:8080", ) note, err := client.(ctx, &notesv1.GetNoteRequest{Id: "n1"}) if != nil { log.Fatal(err) } log.Print(note.Title)

Путь unary-вызова от main до ответа

Расставьте по порядку, что происходит между строкой client.GetNote и получением ответа.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Сжатие и лимиты клиента#

Две настройки клиента, которые стоит знать до того, как заметки вырастут до больших сообщений. Первая — сжатие: по умолчанию клиент декларирует, что умеет читать gzip, а включить его на отправку можно опцией connect.WithGZIP() (в свежих версиях библиотеки она разделена на WithSendGzip и WithAcceptGzip). Библиотека сравнивает допустимое сжатие с тем, что объявил сервер, и выбирает рабочий вариант автоматически — в этом преимущество протокола, который договаривается по-HTTP-ному, через Accept-Encoding. Вторая — лимиты размера: опции WithSendMaxBytes и WithReadMaxBytes ограничивают, сколько клиент готов отправить и принять, и защищают от сюрпризов вида «один отчёт на двести мегабайт положил и клиент, и сервер». Симметричные настройки есть и у handler-а — там мы их подробно разберём в уроке про сжатие и производительность, вместе с HTTP/2-нюансами больших стримов. Здесь зафиксируйте принцип: лимиты ставят на обоих концах, клиент — чтобы не отправить лишнего и не принять внезапный поток, сервер — чтобы не принять его. Ошибки превышения приезжают клиенту тем же *connect.Error с кодом resource_exhausted — первое пересечение этой пары уроков, но далеко не последнее.

Карточки: клиент Connect

Термин

NewNoteServiceClient

нажмите, чтобы перевернуть
Определение

Сгенерированный конструктор клиента: принимает *http.Client, базовый адрес сервера и опции ClientOption, возвращает интерфейс с типизированными методами по rpc-объявлениям схемы.

1 / 6

Что дальше по курсу#

Мы научились вызывать Connect-сервисы из Go: конструктор клиента, три протокола, транспорт, метаданные, контекст. Но любой реальный вызов рано или поздно заканчивается не ответом, а ошибкой — и здесь у Connect есть свой словарь: шестнадцать кодов gRPC, их отображение в HTTP-статусы, *connect.Error с методами Code и Message и правила выбора кода, о которые спотыкаются даже опытные команды. Следующий урок — «Обработка ошибок» — целиком посвящён этой стороне жизни: от connect.NewError в хендлере до корректной обработки на клиенте. После ошибок возьмёмся за перехватчики — единое место для логирования, метрик и авторизации на обоих концах провода; затем стриминг, браузерные клиенты и buf-тулчейн. Программа всех двадцати пяти уроков живёт на странице курса. А если хочется сравнить клиентский опыт с классикой — загляните в урок про gRPC-клиент из курса по gRPC: как увидите, переход почти незаметен, и это лучший показатель качества API Connect.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Плагин генерирует и сервер, и клиент: NewNoteServiceClient(httpClient, baseURL, options...) возвращает типизированный интерфейс по rpc-объявлениям схемы.
  • По умолчанию клиент говорит протоколом Connect; опции connect.WithGRPC() и connect.WithGRPCWeb() переключают протокол без смены схемы и кода вызова.
  • gRPC-протокол требует HTTP/2: https-адрес поднимает его сам, для cleartext нужен http2.Transport с AllowHTTP и подменённым DialTLSContext.
  • Метаданные — обычные HTTP-заголовки: generic-клиент ставит их через connect.NewRequest().Header(), сгенерированный — через клиентский interceptor.
  • Дедлайны и отмена живут в контексте и едут по проводу: connect-timeout-ms у Connect-протокола, grpc-timeout у gRPC; обе стороны останавливаются.
  • Любая ошибка вызова — это *connect.Error: код, сообщение и цепочка оригинала; разбор кодов — тема следующего урока.
  • Generic connect.NewClient[Req, Res] с CallUnary закрывает вызовы без сгенерированного кода: тесты, инструменты, произвольные пути.

Домашнее задание

Соберите рабочий сценарий «клиент + сервер» на базе notes.v1 из третьего урока: сервер запускается на localhost:8080, клиентская программа в отдельном пакете cmd/client вызывает GetNote через http.DefaultClient и печатает заголовок заметки. Затем проделайте три эксперимента: включите connect.WithGRPC() без http2.Transport и зафиксируйте ошибку транспорта; добавьте http2.Transport с AllowHTTP и убедитесь, что вызов по gRPC-протоколу проходит; поставьте context.WithTimeout на 1 наносекунду и понаблюдайте код deadline_exceeded. Напишите в комментариях к коду, какой протокол выбрали бы для внутреннего CLI-инструмента и почему — эта формулировка пригодится в итоговом проекте курса.

Личные заметкиПусто
Хранятся только в вашем браузере (Connect · Клиенты: подключение из Go)

Вопросы и ответы по уроку

Какой http.Client использовать для Connect-клиента?

Для Connect-протокола и gRPC-Web достаточно http.DefaultClient — они живут на HTTP/1.1 и HTTP/2. Для протокола gRPC нужен HTTP/2: на https сработает DefaultClient, для локального cleartext соберите http.Client с http2.Transport и AllowHTTP.

Как передать заголовки (токен, request id) в unary-вызов?

У generic-клиента — через connect.NewRequest и req.Header().Set. У сгенерированного клиента unary-метод принимает только message, поэтому заголовки ставит клиентский interceptor через WithInterceptors — разберём в седьмом уроке.

Можно ли одним клиентом общаться и с gRPC-сервером, и с Connect-сервером?

Да, но по разным правилам: с Connect-сервером — дефолтным протоколом, с чистым gRPC-сервером — опцией WithGRPC и HTTP/2-транспортом. Если сервер собран на Connect, он сам понимает все три протокола, и клиент настраивать не нужно.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

Fyne25 уроков~13 чпрактика25 уроков уже доступны

Fyne: десктопные и мобильные GUI на Go

Курс Fyne: окна, виджеты, контейнеры, canvas, темы, binding, сборка под мобильные и десктоп. 25 уроков и итоговый менеджер задач.

  • Создавать окна и интерфейсы из виджетов
  • Управлять компоновкой: Box, Grid, Form
  • Рисовать примитивами canvas и строить свои виджеты
  • Связывать данные через binding