Содержание урока · 12 разделов
- grpc server streaming: один запрос, поток ответов
- Серверная сторона: Send, nil и никакого io.EOF
- stream.Context(): где живёт контекст стрима
- Клиентская сторона: Recv в цикле до io.EOF
- Как код ошибки доезжает из середины потока
- Unary и server streaming: шесть различий
- Send под капотом: flow control и потерянные хвосты
- Чек-лист server streaming
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Ровно такую строку вы видели в сгенерированном файле с первых уроков курса: rpc WatchOrders (WatchRequest) returns (stream OrderEvent) — и всё это время сервер отвечал на неё кодом Unimplemented, потому что реализованы были только CreateOrder и GetOrder. В шестом уроке про unary мы обещали, что WatchOrders перестанет быть заглушкой, — сегодня сдержим слово. Клиентская половина у вас почти готова: в уроке про gRPC-клиент уже мелькала сигнатура WatchOrders, возвращающая grpc.ServerStreamingClient[OrderEvent], — не хватало лишь цикла чтения. Серверную половину спроектируем с нуля: как отправлять события по одному, когда останавливаться, что вернёт Send, когда клиент ушёл, и почему у этого хендлера, в отличие от unary, вообще нет параметра ctx. Заодно закроем пару заблуждений, которые тянутся из туториалов: про io.EOF «на сервере» и про SendAndClose, который тут ровно ни при чём.
grpc server streaming: один запрос, поток ответов#
Server streaming — вторая из четырёх моделей обмена gRPC и первая стриминговая в курсе: клиент отправляет ровно одно сообщение-запрос, а сервер в ответ шлёт последовательность сообщений — ноль, одно или сколько пожелает, — после чего закрывает поток итоговым статусом. В .proto это слово stream справа от returns: rpc WatchOrders (WatchRequest) returns (stream OrderEvent) читается как «один WatchRequest на входе, поток OrderEvent на выходе». Русскоязычный запрос «стрим сервер go» описывает ровно эту конструкцию: поток ведёт сервер, клиент только слушает и в любой момент волен уйти. Кардинальность здесь не жёсткая, как в unary, а свободная, и это стоит проговорить через контраст: unary требовал ровно один ответ — лишнее сообщение каралось codes.Internal, а ноль ответов тоже считался нарушением; серверному стриму и ноль, и много сообщений — легальные сценарии, никаких проверок кардинальности на клиенте не включается. Подписка на события, за которую отвечает WatchOrders, потому и стриминг: заказ 42 сегодня проходит путь создан → оплачен → отгружен за минуту и порождает три события, а может зависнуть в статусе «оплачен» на сутки — одним сообщением такой ответ заранее не спланируешь. Server streaming — правильный инструмент для всех трёх поводов из таблицы шестого урока: события в реальном времени, ответы неограниченного размера и ответы, длина которых неизвестна в момент запроса. Два оставшихся повода — поток от клиента и полноценный дуплекс — достанутся восьмому и девятому урокам.
Контракт WatchOrders: proto и сгенерированные подписи
GO// api/shop/v1/order.proto — контракт из второго урока, без изменений.
service OrderService {
// stream справа от returns — серверный стрим:
// на входе один WatchRequest, на выходе поток OrderEvent.
rpc WatchOrders (WatchRequest) returns (stream OrderEvent);
}
message WatchRequest {
OrderStatus status = 1; // фильтр подписки: UNSPECIFIED — все события
}
message OrderEvent {
Order order = 1; // заказ на момент события
string kind = 2; // тип события: "created", "paid", "shipped"
}
// Сгенерированный order_grpc.pb.go — обе стороны одного метода.
// Клиент: контекст по-прежнему в вызове, вместо ответа — интерфейс стрима.
WatchOrders(ctx context.Context, in *WatchRequest, opts ...grpc.CallOption) (
grpc.ServerStreamingClient[OrderEvent], error)
// Сервер: параметра ctx НЕТ — контекст вызова достаётся из стрима.
WatchOrders(*WatchRequest, grpc.ServerStreamingServer[OrderEvent]) errorДва отличия от unary видны сразу. Первое: у клиента контекст остался на месте — дедлайн ставится тем же context.WithTimeout, а вместо готового ответа метод возвращает интерфейс grpc.ServerStreamingClient[OrderEvent], через который ответы читаются по одному. Второе, и на этом спотыкается почти каждый: у серверного хендлера нет параметра ctx. Контекст вызова — с дедлайном клиента, отменой и метаданными — доступен только через stream.Context(). Это следствие генерации: интерфейс фиксирует сигнатуру, а сигнатура server-streaming метода — два аргумента, запрос и стрим. Псевдоним OrderService_WatchOrdersServer = grpc.ServerStreamingServer[OrderEvent] — просто старое имя нового дженерика, Generator оставил его ради обратной совместимости кода.
Серверная сторона: Send, nil и никакого io.EOF#
Серверный интерфейс grpc.ServerStreamingServer[OrderEvent] в дженериках живёт с grpc-go 1.64: раньше файл stream.go описывал лишь не-generic ServerStream с SendMsg(m any), а protoc-gen-go-grpc создавал под каждый метод собственный интерфейс вида OrderService_WatchOrdersServer с Send(OrderEvent) error и встроенным grpc.ServerStream. Современный генератор опирается на обобщённые интерфейсы из stream_interfaces.go, и под старым именем теперь прячется псевдоним нового типа — код, написанный в старом стиле с Send(OrderEvent), продолжает собираться без изменений. В интерфейсе два слоя: Send(*OrderEvent) error — «отправь очередное сообщение в поток» — и встроенный ServerStream с методами Context, SetHeader, SendHeader и SetTrailer. Документация формулирует контракт завершения дословно: «To terminate the response stream, return from the handler method and return an error from the status package, or use nil to indicate an OK status code» — вернул nil, поток закрылся со статусом OK; вернул ошибку из пакета status, клиент получит её код и сообщение. Из этого следует самое живучее заблуждение темы: io.EOF на сервере не нужен. io.EOF — это клиентский сигнал «поток кончился успешно», его возвращает Recv; хендлер не посылает никакого явного «конца» — конец это возврат из функции. По той же причине здесь не место SendAndClose: этот метод живёт в ClientStreamingServer и закрывает unary-ответ после потока запросов — его разбор честно ждёт восьмой урок про client streaming.
Хендлер WatchOrders: подписка на события заказа
GO// internal/server/order.go — WatchOrders больше не заглушка Unimplemented.
func (s *OrderServiceServer) WatchOrders(
req *pb.WatchRequest, stream pb.OrderService_WatchOrdersServer) error {
// Учебный источник событий: в реальном сервисе это шина или очередь,
// куда CreateOrder и переходы статуса заказа 42 уже пишут.
ch := make(chan *pb.OrderEvent, 16)
s.subscribe(ch)
defer s.unsubscribe(ch)
for {
select {
case <-stream.Context().Done():
// Клиент ушёл: отменил контекст или истёк его дедлайн.
// Библиотека переведёт ошибку контекста в статус вызова.
return stream.Context().Err()
case ev := <-ch:
// Фильтр подписки из запроса: UNSPECIFIED — все события.
if req.Status != pb.OrderStatus_ORDER_STATUS_UNSPECIFIED &&
ev.Order.GetStatus() != req.Status {
continue
}
// Каждое событие — отдельное сообщение в потоке.
if err := stream.Send(ev); err != nil {
// Стрим сломан (клиент отключился, сеть оборвалась):
// слать дальше некуда, возвращаем ошибку как есть.
return err
}
// Заказ дошёл до финального статуса — подписка исполнена.
if ev.Order.GetStatus() == pb.OrderStatus_ORDER_STATUS_SHIPPED {
return nil // успешный конец: клиент увидит io.EOF
}
}
}
}Читаем по строкам: select с двумя ветками — сердце любого живого хендлера. Ветка Done срабатывает, когда контекст стрима закрыт, — без неё хендлер будет вечно читать канал, который никто не слушает, и GracefulStop из серверного урока никогда не дождётся. Ошибку Send мы не глотаем: после сбоя стрим непригоден, и документация прямо советует возвращаться. Финальный nil — не формальность, а сам контракт: именно он превращается в grpc-status 0 в trailers. Замените учебный канал на шину событий — каркас select + Send + nil останется тем же.
stream.Context(): где живёт контекст стрима#
Unary-хендлер получал контекст вторым аргументом, и дедлайн из заголовка grpc-timeout лежал в ctx.Deadline(). В server streaming параметра нет — это не потеря, а перенос: stream.Context() возвращает контекст именно этого вызова, со всем содержимым. Внутри всё знакомое по шестому уроку: дедлайн клиента, который уехал заголовком grpc-timeout, отмена, значения, peer. Отсюда главный вопрос проектирования подписок — отсоединение. Подписка — это обязательство сервера слать события, пока клиент слушает; клиент же может уйти в любой момент: закрыть приложение, вызвать cancel(), истечь дедлайном. Первое уведомление приходит в ветку <-stream.Context().Done(): хендлер обязан прекратить работу и вернуться. Если же он проспал момент и позвал Send после отсоединения — Send вернёт ошибку, и это будет статусная ошибка codes.Canceled с сообщением «context canceled»: транспорт видит закрытый HTTP/2-поток, streamContextErr разворачивает ошибку контекста в статус, а toRPCErr пропускает статусные ошибки как есть. Кода Internal здесь не будет — уход клиента для стрима не авария, а штатная старость: Internal зарезервирован для неожиданных поломок самой библиотеки. Любопытная деталь исходников: отложенный код в SendMsg при неудаче сам записывает этот статус итогом вызова — хендлеру даже возвращаться не обязательно, чтобы клиент узнал правду. И напомним предохранитель из серверного урока: вечный стрим без дедлайна не даст GracefulStop вернуться — подписки обязаны уметь кончаться.
Клиент оборвал соединение. Что вернёт Send?
Прочитайте код и выберите, что он выведет:
// Хендлер читает события из канала и шлёт их клиенту.
// Клиент в этот момент уже вызвал cancel() и вышел из программы.
select {
case <-stream.Context().Done():
fmt.Println("контекст закрыт")
case ev := <-ch:
err := stream.Send(ev)
fmt.Println(status.Code(err))
}Проверка: чем завершить поток
Сервер отправил все события. Как правильно закрыть server-streaming вызов?
Клиентская сторона: Recv в цикле до io.EOF#
Клиентский интерфейс зеркально лаконичен: grpc.ServerStreamingClient[OrderEvent] — один метод Recv() (*OrderEvent, error) плюс унаследованные Header, Trailer и Context. Каждый вызов Recv блокируется до следующего сообщения и возвращает его; документация интерфейса сводит все исходы к двум. Первый: «If io.EOF is returned, the stream has terminated with an OK status» — сервер вышел с nil, все сообщения доставлены, подписка закрыта успешно. Второй: «Any other error is compatible with the status package and indicates the RPC's status code and message» — любая другая ошибка статусная, в ней код и сообщение вызова, и ветвиться по ним нужно через status.Code, как вы уже делаете с unary-вызовами. Идиома чтения идёт из официального примера route_guide в репозитории grpc-go: бесконечный for, внутри две проверки — err == io.EOF означает чистый выход из цикла, прочая ошибка означает разбор и завершение. После первой не-EOF ошибки звать Recv снова бессмысленно: вызов завершён, ошибка будет та же. Под капотом GenericClientStream.Recv — тонкая обёртка над ClientStream.RecvMsg: вся транспортная машинерия из шестого урока — пятибайтовые префиксы длины, окна, trailers — крутится тем же кодом, что и в unary; разница в том, что RecvMsg здесь зовут добровольно и много раз, а не два раза под капотом Invoke.
Цикл чтения: io.EOF — не ошибка
GO// cmd/client/main.go: подписка на события заказа.
// ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
// defer cancel()
//
// stream, err := client.WatchOrders(ctx, &pb.WatchRequest{
// Status: pb.OrderStatus_ORDER_STATUS_UNSPECIFIED, // все события
// })
// if err != nil {
// log.Fatalf("WatchOrders: %v", err) // вызов даже не открылся
// }
for {
ev, err := stream.Recv()
switch {
case err == io.EOF:
// Сервер вернул nil: поток завершён со статусом OK.
fmt.Println("сервер закрыл поток: событий больше не будет")
return
case err != nil:
// Статусная ошибка из потока: NotFound, Canceled,
// DeadlineExceeded — ветвимся по коду, как в unary.
log.Fatalf("подписка прервана: %v (код %v)", err, status.Code(err))
}
fmt.Printf("событие %s: заказ %d в статусе %s
",
ev.GetKind(), ev.GetOrder().GetId(), ev.GetOrder().GetStatus())
}Каркас совпадает с printFeatures официального примера route_guide — в репозитории grpc-go этот цикл служит эталоном чтения стрима уже много лет. Порядок проверок важен: io.EOF — не ошибка, а штатный конец, поэтому проверяется первым; status.Code(err) достанет код из статусной ошибки для журнала и решений. Геттеры GetKind и GetOrder — сгенерированные nil-безопасные помощники protobuf из урока про синтаксис: без них каждое чтение поля пришлось бы защищать проверкой на nil.
Как код ошибки доезжает из середины потока#
Разбор ошибок Recv стоит один раз увидеть изнутри, потому что статус в стриминге доезжает не сразу. Пока сервер шлёт сообщения, клиенту едет чистый protobuf; итоговый статус — grpc-status и grpc-message в trailers, как в шестом уроке, — записывается в самом конце, после последнего сообщения потока. Функция recv в stream.go разворачивает это в три ветки. Первая: транспорт сообщил io.EOF — данные кончились; библиотека читает Status() стрима, и если там не OK, возвращает именно эту статусную ошибку — это случай, когда сервер вернул status.Error до первого сообщения или посреди потока. Вторая: статус оказался OK — Recv возвращает io.EOF, клиент спокойно выходит из цикла; для server-streaming методов пустой поток легален, поэтому проверки кардинальности не включаются вовсе — знаменитое «cardinality violation: received no response message from non-server-streaming RPC» срабатывает только когда ServerStreams: false, то есть в unary. Третья ветка: сбой транспорта посреди потока — его тянет через toRPCErr, и клиент получает статусные Unavailable или Unknown вместо тихого зависания. Практическое следствие одно, но важное: статусная ошибка может вылететь на любом Recv — и на первом, если сервер отказал сразу, и на десятом, если посреди подписки истёк дедлайн или упала сеть. Клиентский цикл обязан уметь завершиться в любой момент, а коды обработки ошибок не должны предполагать, что «до первой ошибки всё точно дошло».
Сервер отказал до первого события
Прочитайте код и выберите, что он выведет:
// Хендлер: доступ запрещён — стрим открыт, сообщений ещё не было.
func (s *OrderServiceServer) WatchOrders(
req *pb.WatchRequest, stream pb.OrderService_WatchOrdersServer) error {
if !s.canWatch(req) {
return status.Error(codes.PermissionDenied,
"нет доступа к событиям заказов")
}
// ...
}
// Клиент: первое же чтение из свежего стрима.
ev, err := stream.Recv()Проверка: пустой поток
Сервер немедленно вышел с return nil, не послав ни одного OrderEvent. Что увидит клиент?
Соблазн вернуть io.EOF «чтобы клиент вышел из цикла» понятен и вреден. Библиотека вызывает status.FromError: io.EOF — не статусная ошибка; дальше status.FromContextError: это не DeadlineExceeded и не Canceled — итог codes.Unknown с текстом «EOF». Клиент, который честно ждёт io.EOF как знак успеха, уйдёт в ветку ошибки и, возможно, начнёт ретраи там, где никакой ошибки не было. На сервере сигнал конца потока ровно один — return nil (или статусная ошибка для неуспеха); io.EOF живёт на другой стороне провода, в клиентском Recv.
Unary и server streaming: шесть различий#
| Вопрос | Unary (шестой урок) | Server streaming (этот урок) |
|---|---|---|
| Схема | rpc GetOrder (…) returns (Order) | rpc WatchOrders (…) returns (stream OrderEvent) |
| Хендлер | GetOrder(ctx, req) (*pb.Order, error) | WatchOrders(req, stream) error — контекст в stream.Context() |
| Сколько ответов | ровно один: ноль или два — cardinality violation | ноль и больше: пустой поток легален |
| Отправка | ответ — возврат из хендлера | stream.Send(событие) на каждое сообщение |
| Завершение | return order, nil | return nil — OK; return status.Error(…) — отказ |
| Сигнал конца у клиента | ответ как результат Invoke | io.EOF из stream.Recv() |
Send под капотом: flow control и потерянные хвосты#
Send выглядит как fire-and-forget, но у него есть характер, и документация проговаривает его в три предложения. Первое: SendMsg блокируется, пока не появится достаточный flow control — место в HTTP/2-окне, чтобы поставить сообщение в транспорт; медленный клиент и объёмные сообщения делают хендлер медленным самим по себе, без всяких искусственных задержек: если получатель не читает, отправка упрётся в окно и встанет. Второе: «SendMsg does not wait until the message is received by the client» — «ушло в транспорт» не значит «получено»; подтверждений доставки на уровне библиотеки нет. Третье, важнейшее для дизайна: «An untimely stream closure may result in lost messages» — несвоевременное закрытие потока может потерять сообщения: клиент ушёл, а хендлер уже отдал последние три события в буфер. Отсюда архитектурное правило: стрим годится для событий, прогресса и логов, где каждое сообщение самодостаточно, и не годится для сценариев вида «последнее сообщение — самое важное»: итог вызова живёт в статусе trailers, а не в последнем сообщении потока. Технические границы из того же исходника: сообщение больше maxSendMessageSize вернёт codes.ResourceExhausted с текстом «trying to send message larger than max», а звать Send из двух горутин на одном стриме запрещено — конкурентная отправка это территория девятого урока про bidi.
Проверка: что гарантирует Send
stream.Send(ev) вернул nil. Что это означает?
Сквозной клиент: подписка с дедлайном
GO// cmd/client/main.go — сквозной пример целиком.
func main() {
// Канал без сетевых операций — как в клиентском уроке.
conn, err := grpc.NewClient("localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("канал: %v", err)
}
defer conn.Close()
client := pb.NewOrderServiceClient(conn)
// Дедлайн — предохранитель подписки: стрим обязан кончаться.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
stream, err := client.WatchOrders(ctx, &pb.WatchRequest{})
if err != nil {
log.Fatalf("WatchOrders: %v", err)
}
for {
ev, err := stream.Recv()
switch {
case err == io.EOF:
// Сервер вернул nil — все события доставлены.
fmt.Println("сервер закрыл поток со статусом OK")
return
case err != nil:
// Свой же дедлайн тоже приезжает статусной ошибкой:
// DeadlineExceeded — истёк срок, Canceled — отменили.
fmt.Printf("подписка прервана: код %v
", status.Code(err))
return
}
fmt.Printf("[%s] заказ %d → %s
",
ev.GetKind(), ev.GetOrder().GetId(), ev.GetOrder().GetStatus())
}
}Симметрия, ради которой всё затевалось. Клиент ставит дедлайн — сервер видит его в stream.Context(); сервер возвращает nil — клиент выходит по io.EOF; клиент уходит по cancel() или дедлайну — сервер ловит ctx.Done(), а его следующий Send приносит Canceled. Одна и та же ошибка контекста честно видна обеим сторонам — это и есть отсоединение по ctx в полном цикле. Для вечной подписки вместо WithTimeout берут context.WithCancel и отменяют по пользовательскому решению или сигналу процесса — механика чтения не меняется ни на символ.
Чек-лист server streaming#
- Схема: stream справа от returns — один запрос, поток ответов; ноль сообщений легален.
- Сервер шлёт через stream.Send(*ev) и завершает вызов возвратом: nil — OK, статусная ошибка — отказ.
- io.EOF — клиентский сигнал конца; вернёте его из хендлера — клиент получит codes.Unknown с текстом «EOF».
- Контекст вызова — stream.Context(): дедлайн и отмена клиента; проверяйте Done() между Send.
- Send после отсоединения вернёт статус (обычно codes.Canceled) — дальше слать бессмысленно, возвращайтесь.
- Клиент: Recv в цикле до io.EOF; прочая ошибка — статусная, ветвьтесь по status.Code.
- Send не ждёт доставки: обрыв может потерять хвост — важный итог кладите в статус вызова, а не в последнее сообщение.
Соберите клиентский цикл подписки
Восстановите клиентскую половину WatchOrders: дедлайн, вызов и чтение потока до конца.
Жизненный цикл server-streaming вызова
Расставьте события подписки WatchOrders — от вызова клиента до чистого завершения обеих сторон.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Что дальше по курсу#
Одно честное замечание про формат: у самоучителя есть песочница с интерпретатором Go, но сеть в браузере недоступна — поднять сервер и читать из него стрим оттуда невозможно, поэтому runner-ов в этом уроке и во всём gRPC-курсе нет. Всё, что здесь разобрано, проверяется локально за минуты: сервер из урока 04, клиент из урока про gRPC-клиент — и WatchOrders оживёт: попробуйте отменить контекст после второго события и посмотрите, что вернёт Send на сервере. Дальше по маршруту: восьмой урок — client streaming, поток разворачивается в другую сторону, клиент шлёт последовательность сообщений, сервер отвечает одним итогом через SendAndClose — и его вы уже не спутаете с завершением серверного стрима; следом девятый урок — bidi, где обе стороны говорят одновременно. Метаданные и токены авторизации — в уроке про metadata, глубокие дедлайны и распространение отмены — в уроке про дедлайны. А механику unary — grpc-timeout, trailers и статусные ошибки полезно перечитать перед восьмым уроком: стриминг построен ровно на ней, только сообщений стало много. Полный маршрут курса — на странице gRPC-Go.
Карточки: server streaming под микроскопом
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Server streaming — один запрос, поток ответов: в .proto слово stream справа от returns; WatchOrders наконец реализован вместо заглушки Unimplemented.
- Сервер: stream.Send(*OrderEvent) на каждое событие; завершение — return nil (клиент получит io.EOF) или статусная ошибка; SendAndClose не нужен — это client streaming.
- io.EOF — клиентский сигнал конца потока; на сервере он превращается в codes.Unknown: конец обозначают nil, а не io.EOF.
- Контекст вызова живёт в stream.Context(): дедлайн и отмена клиента видны серверу; Send после отсоединения вернёт codes.Canceled.
- Пустой поток легален: проверка cardinality violation включается только для не-стримовых методов; подписка без событий — нормальный ответ.
- Клиент: Recv в цикле, io.EOF — чистый выход, прочая ошибка — статусная: код приходит и до первого сообщения, и посреди потока.
- Send блокируется на flow control и не гарантирует доставки: обрыв может съесть хвост — важный итог кладите в статус вызова.
Домашнее задание
Оживите WatchOrders по раскладке серверного урока. Первый шаг: реализуйте хендлер с каналом событий и select по stream.Context().Done(), отправьте три события заказа 42 — created, paid, shipped — и убедитесь, что клиент читает их циклом Recv и выходит по io.EOF. Второй: поставьте клиенту таймаут в одну секунду, а серверу — паузу в три секунды между событиями: клиент получит DeadlineExceeded, а сервер увидит ctx.Done() и вернётся из хендлера; затем замените таймаут на context.WithCancel, отмените подписку вручную и прочитайте codes.Canceled в Send на сервере. Третий: верните из хендлера io.EOF вместо nil и посмотрите на клиенте codes.Unknown с текстом «EOF»; верните nil и сравните. Наконец, добавьте фильтр по req.Status: подпишитесь только на ORDER_STATUS_PAID и проверьте пустой поток — клиент должен выйти по io.EOF, не получив ни одного события.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем server streaming отличается от repeated-поля в ответе?
Repeated собирает весь список в одно сообщение: упрётесь в потолок размера сообщения и будете ждать конца всего списка. Server streaming шлёт каждое событие отдельным сообщением сразу, как только оно готово, количество сообщений заранее не ограничено, а клиент может прервать подписку в любой момент.
Почему у хендлера нет параметра ctx?
Сгенерированная сигнатура server-streaming метода — запрос и стрим, без контекста. Контекст вызова доступен через stream.Context(): там дедлайн клиента из grpc-timeout, отмена и метаданные. Это отличие от unary, где ctx приходит вторым аргументом.
Нужно ли клиенту звать CloseSend после серверного стрима?
Нет. В server streaming клиент не отправляет поток, и документация интерфейса прямо просит не трогать прочие методы ClientStream. Подписку закрывает сервер возвратом nil, а клиент — отменой контекста или выходом из цикла Recv; CloseSend нужен в client streaming, чтобы пометить конец последовательности запросов.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Connect: современный RPC от Buf
Курс Connect (Buf): протоколы, клиенты, ошибки, interceptors, браузерные клиенты, buf CLI и BSR. 25 уроков и итоговый проект API.
- Строить сервисы на connectrpc.com/connect
- Понимать три протокола и выбирать правильный
- Настраивать buf: lint, breaking changes, generate
- Писать interceptors и обрабатывать ошибки