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

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

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

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

Типы вызовов: unary

CreateOrder и GetOrder уже прошли полный круг: сервер из четвёртого урока отвечает, клиент из пятого — получает, и оба этих метода — unary, самый ходовой тип вызовов gRPC. Здесь разбираем grpc unary системно: модель запрос-ответ и все четыре типа вызовов рядом, анатомия unary-вызова поверх HTTP/2 — от заголовка grpc-timeout до trailers с grpc-status, — таймауты через context.WithTimeout с двух сторон провода и модель простых ошибок: status.Error на сервере, status.Code на клиенте. На этом фундаменте дальше строятся три стриминговых урока, дедлайны, статус-коды и ретраи.

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

Пять уроков позади, и главный результат этого пути стоит сформулировать честно: вы уже вызывали unary-методы, просто не называли их так. CreateOrder и GetOrder из схемы заказов — по одному сообщению на входе и на выходе, и обе пары «хендлер + вызов» вы написали своими руками: сервер — в уроке про первый gRPC-сервер, клиент — в уроке про gRPC-клиент. Оставалось небрежное чувство, что вызов — это какой-то магический чёрный ящик: байты уходят, структура возвращается, а что происходит между Invoke и возвратом из хендлера — неясно. Этот урок закрывает пробел: соберём все четыре типа вызовов в одну таблицу, разберём unary-вызов покадрово — что именно едет по HTTP/2, — вытащим на свет заголовки grpc-status, grpc-message и grpc-timeout, поставим таймауты с обеих сторон и договоримся, какие ошибки наружу можно, а какие нельзя.

grpc unary: модель запрос-ответ#

Unary — это RPC в чистом виде: клиент отправляет одно сообщение-запрос, сервер отвечает ровно одним сообщением-ответом, и на этом вызов закончен. В коде это выглядит как обычная функция: аргументы на входе, результат на выходе, — и документация Invoke формулирует контракт буквально: «sends the RPC request on the wire and returns after response is received» — отправляет запрос на провод и возвращается после получения ответа. За простотой стоит строгая дисциплина кардинальности, которую фреймворк проверяет за вас: сгенерированный unary-метод — это Invoke над дескриптором StreamDesc с ServerStreams: false и ClientStreams: false, и внутри invoke клиент после первого RecvMsg зовёт его второй раз — за trailers. Если сервер по недосмотру выслал два сообщения, второй RecvMsg вернёт не io.EOF, а очередное сообщение, и клиент получит codes.Internal с сообщением «cardinality violation: expected <EOF> for non server-streaming RPCs, but received another message» — нарушение кардинальности карается на месте. После отправки единственного запроса клиент вызывает CloseSend — сигнал «других сообщений не будет». Тот самый unary call, о котором пишут англоязычные туториалы, — это и есть эта модель: один Request, один Response, ничего посередине. И важная поправка на масштаб: unary — свойство метода, а не соединения. В одном HTTP/2-соединении мультиплексируются десятки параллельных unary-вызовов, так что «запрос-ответ» не значит «медленно и по очереди» — мультиплексирование потоков из первого урока здесь и работает.

Типы вызовов: четыре модели обмена#

Вид вызоваВ нашей схемеКлиент → серверСервер → клиентСгенерированный метод клиента
unaryCreateOrder, GetOrderодно сообщениеодно сообщение(*pb.Order, error) — Invoke
server streamingWatchOrdersодно сообщениепоток OrderEventgrpc.ServerStreamingClient[OrderEvent]
client streamingнет в схеме; пример: UploadReceipt(stream ReceiptChunk) returns (ReceiptSummary)поток чанководна сводкаgrpc.ClientStreamingClient[ReceiptChunk, ReceiptSummary]
bidirectionalнет в схеме; пример: чат поддержки ChatTickets(stream TicketMsg) returns (stream TicketMsg)потокпотокgrpc.BidiStreamingClient[TicketMsg, TicketMsg]

Типы вызовов: как выбирать#

Таблицу стоит читать не как справочник, а как чек-лист проектирования. Дефолт — unary: большинство операций сервиса заказов — это «спросил — ответил», и стриминг там не нужен ровно до тех пор, пока не появится требование, которое обычным запрос-ответом не выражается. Таких требований три классических. Первое: список неограниченного размера — отдавать каталог из миллиона позиций одним unary-сообщением нельзя, упрётесь в потолок сообщения (по умолчанию 4 мегабайта, помните ResourceExhausted?). Второе: события в реальном времени — курс валют, логи, прогресс долгой операции; клиенту нужно получать их по мере появления, а не одним пакетом в конце. Третье: большая передача по частям — файл заливается чанками. Распознаются типы вызовов в .proto по слову stream: нет слова — unary, справа — серверный стрим, слева — клиентский, с обеих сторон — двунаправленный; это правило из урока про proto3, и теперь к нему прилагаются конкретные Go-типы из таблицы выше. Каждому стриминговому виду в курсе посвящён отдельный урок: седьмой — server streaming, где WatchOrders наконец перестанет быть заглушкой, восьмой — client streaming, девятый — bidi. А этот урок добивает unary до конца: провод, таймауты, ошибки.

Так unary-вызов GetOrder выглядит на проводе

GO
// Путь вызова GetOrder(ctx, &pb.GetOrderRequest{Id: 42})
// по HTTP/2 — реконструкция кадров (упрощённо, без HPACK-сжатия).

// 1. Клиент открывает поток и отправляет заголовки запроса:
//    :path         /shop.v1.OrderService/GetOrder
//    :method       POST
//    content-type  application/grpc
//    grpc-timeout  2S
//    te            trailers

// 2. Клиент шлёт тело запроса: 5 байт префикса — 1 байт флага
//    сжатия + 4 байта длины сообщения в big-endian — и затем
//    байты protobuf-сообщения GetOrderRequest.
//    Флагом END_STREAM клиент закрывает своё направление:
//    сообщений от него больше не будет.

// 3. Сервер выполняет хендлер и отвечает. Сначала заголовки ответа:
//    :status       200
//    content-type  application/grpc
//    затем РОВНО ОДНО сообщение Order — тот же префикс и protobuf,
//    затем trailers с итогом вызова:
//    grpc-status   0
//    grpc-message  (пусто: ошибки нет)

Три кадра слева направо — и три вещи, которые стоит заметить. Первая: сообщение не «просто байты» — перед каждым лежит пятёрка байтов префикса, где объявлены сжатие и длина, поэтому получатель знает, где сообщение кончается. Вторая: :path — тот самый FullMethodName «/shop.v1.OrderService/GetOrder» из клиентского урока, а content-type — application/grpc (с подтипом кодека: application/grpc+proto). Третья: итоговый статус едет не в заголовках ответа, а в trailers — кадре заголовков, закрывающем поток.

grpc-status, grpc-message, grpc-timeout: заголовки протокола#

Заголовки из трассировки выше — не произвольные строки, а элементы спецификации gRPC поверх HTTP/2, и в grpc-go за каждым стоит конкретный код. Начнём с конца — со статуса. В http2_server.go метод WriteStatus собирает trailers: поле «grpc-status» получает десятичное представление кода (strconv.Itoa от номера из перечисления codes — успешный вызов это grpc-status: 0), поле «grpc-message» — текст сообщения, пропущенный через encodeGrpcMessage: печатные ASCII-байты проезжают как есть, всё остальное кодируется процентами, а невалидный UTF-8 заменяется символом-заменой — сообщение никогда не сломает кадр заголовков. Если сервер приложил к статусу детали, они едут отдельным заголовком grpc-status-details-bin в base64 — это поле станет главным в уроке про статус-коды и details. Теперь начало — таймаут. Клиент отправляет дедлайн вызова в заголовке «grpc-timeout» как число с суффиксом единицы: n, u, m, S, M, H — от наносекунд до часов. Функция EncodeDuration в internal/grpcutil выбирает наименьшую единицу, в которой значение умещается в восемь цифр: две секунды уезжают как «2S», 250 миллисекунд — как «250m». И примечательная деталь из исходников http2_client.go: клиент шлёт grpc-timeout всегда, даже когда дедлайна нет — комментарий гласит «Send out timeout regardless its value. The server can detect timeout context by itself»: сервер сам разберётся со своим контекстом. Trailers нужны потому, что итог вызова известен не всегда в момент отправки тела: для unary они идут следом почти вплотную, а для стримингов — после последнего сообщения потока; механика одна и та же.

Какое значение уедет в grpc-timeout?

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

// Клиент ставит срок вызова в 250 миллисекунд.
// Что сервер увидит в заголовке grpc-timeout?

ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond)
defer cancel()

_, err := client.GetOrder(ctx, &pb.GetOrderRequest{Id: 42})

Проверка: где едет итоговый статус

В каком HTTP/2-кадре клиент получает grpc-status успешного unary-ответа?

Таймауты unary: дедлайн едет вместе с запросом#

Как ставить срок вызову, вы уже знаете из клиентского урока: context.WithTimeout + defer cancel. Здесь важнее другое — что дедлайн делает дальше. Первый факт: он уезжает на сервер. Заголовок grpc-timeout заполняется из контекста вызова, и сервер, принимая вызов, восстанавливает дедлайн в своём ctx: ctx.Deadline() хендлера показывает тот же срок, что поставил клиент. Второй факт: клиент не обязан ждать сервера. Как только срок истёк, Invoke завершается ошибкой DeadlineExceeded — конвертация context.DeadlineExceeded в одноимённый код зашита в toRPCErr rpc_util.go, — даже если сервер через секунду пришлёт прекрасный ответ: приниматься он уже не будет. Третий факт: сервер узнаёт об уходе клиента. HTTP/2-поток закрывается, и ctx.Done() в хендлере срабатывает — работа после дедлайна становится чистым сжиганием ресурсов: сервер считает, кодирует и отправляет ответ в закрытый поток. Отсюда правило уважения дедлайна: любую долгую работу дробить на порции и проверять ctx.Done(). Четвёртое — различать два «вызов умер»: DeadlineExceeded означает «истёк срок», а Canceled — «кто-то явно отменил контекст»: пользователь ушёл со страницы, родительский вызов закрылся, отработал таймаут выше по цепочке. Клиент различает их через status.Code и реагирует по-разному: первый — честный ответ «не успели», второй — повод узнать, кто и зачем отменил. И последний штрих: дедлайн наследуется. Производный контекст передаётся дальше, поэтому unary-вызов вашего сервиса к соседнему унаследует срок родительского запроса — а как этот механизм устроен внутри и как задавать дедлайны через WithDeadline, разберёт урок про дедлайны и отмены; grpc.WithTimeout как DialOption, помните, устарел и игнорируется.

Дедлайн с двух сторон: клиент задаёт — сервер уважает

GO
// Клиент (из прошлого урока): срок конкретного вызова — 2 секунды.
// ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
// defer cancel()
// order, err := client.GetOrder(ctx, &pb.GetOrderRequest{Id: 42})

func (s *OrderServiceServer) GetOrder(ctx context.Context, req *pb.GetOrderRequest) (*pb.Order, error) {
        // Дедлайн приехал с запросом: на проводе это grpc-timeout,
        // в коде хендлера — обычный ctx.Deadline().
        if dl, ok := ctx.Deadline(); ok {
                log.Printf("GetOrder(%d): у вызова осталось %v", req.Id, time.Until(dl))
        }

        // Долгую работу дробим на порции и после каждой проверяем ctx.Done().
        for !s.ready(req.Id) {
                select {
                case <-ctx.Done():
                        // Клиент ушёл: истёк срок или вызов отменён.
                        // FromContextError: DeadlineExceeded или Canceled по ctx.Err().
                        return nil, status.FromContextError(ctx.Err()).Err()
                default:
                }
                // ... порция работы: собрать позиции, посчитать сумму ...
        }

        return s.orders[req.Id], nil
}

status.FromContextError — функция из пакета status, которая превращает ошибку контекста в статусный код: context.DeadlineExceeded становится DeadlineExceeded, context.Canceled — Canceled, остальное — Unknown. Хендлер мог бы просто вернуть ctx.Err() как есть — клиент всё равно получит корректные коды, библиотека конвертирует не-статусные ошибки контекста сама, — но явный FromContextError делает намерение читаемым. Важно понимать симметрию: если хендлер игнорирует ctx.Done(), он доработает до конца, но его WriteStatus уйдёт в закрытый поток — потрачены процессор, память и соединение.

Хендлер не уложился. Что увидит клиент?

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

// Хендлер GetOrder делает time.Sleep(5 * time.Second)
// и затем возвращает заказ.
//
// Клиент вызвал с таймаутом в две секунды:

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

_, err := client.GetOrder(ctx, &pb.GetOrderRequest{Id: 42})
fmt.Println("код:", status.Code(err))

Простые ошибки: полный путь status.Error#

Клиентская половина разбора ошибок у вас уже есть — status.Code из прошлого урока. Теперь соберём серверную половину и весь путь целиком. Хендлер возвращает error — дальше слово за библиотекой. В processRPC grpc-go звонит status.FromError(appErr): если ошибка статусная — она же и едет в WriteStatus, то есть в trailers grpc-status/grpc-message. Если не статусная — включается status.FromContextError: context.DeadlineExceeded превратится в код DeadlineExceeded, context.Canceled — в Canceled, а всё прочее — в codes.Unknown с текстом исходной ошибки в grpc-message. Из этой цепочки следуют три практических правила. Правило первое: вернули status.Error(codes.NotFound, ...) — клиент получит NotFound; вернули errors.New или fmt.Errorf — клиент получит Unknown и не сможет ветвиться. Правило второе, тоньше: обёртка fmt.Errorf с %w поверх статусной ошибки код сохраняет — FromError ищет статус в цепочке через errors.As по интерфейсу GRPCStatus, — однако текст сообщения в trailers заменится на текст внешней обёртки: клиент увидит и код, и ваш контекст. Правило третье: достать читаемое сообщение на клиенте — status.Convert(err).Message(), конвертация возвращает *status.Status с методами Code и Message. Полная анатомия кодов — какие существуют, что означает каждый, как прикладывать детали через WithDetails и RetryInfo — это тема отдельного урока про статус-коды; здесь важно, что даже «простые ошибки» — это структурированные статусы на проводе, а не строки в логах.

Три возврата из хендлера — три судьбы на клиенте

GO
// Вариант 1: честный статусный код.
// Клиент видит codes.InvalidArgument и может отреагировать.
if len(req.Items) == 0 {
        return nil, status.Error(codes.InvalidArgument, "заказ без позиций не принимаем")
}

// Вариант 2: обычная ошибка. Клиент получит codes.Unknown,
// а в grpc-message — текст этой ошибки: ветвиться по коду нельзя.
if err := s.payments.Hold(ctx, amount); err != nil {
        return nil, fmt.Errorf("платёжный холт не прошёл: %v", err)
}

// Вариант 3: обёртка %w поверх статуса. Код сохранится (NotFound),
// но grpc-message заменится на текст обёртки:
// «GetOrder(42): заказ не найден».
order, ok := s.lookup(req.Id)
if !ok {
        return nil, fmt.Errorf("GetOrder(%d): %w", req.Id,
                status.Error(codes.NotFound, "заказ не найден"))
}

Разница между вариантами 2 и 3 — в одном символе: %v рвёт связь с статусом, %w сохраняет её через errors.As, и status.Code на клиенте различит NotFound от Unknown. Но контекст ошибки всё равно полезнее кода: лучше сразу вернуть status.Errorf с осмысленным сообщением, чем надеяться на обёртки. Обратите внимание: вариант 1 — ровно та строка, которую вы писали в серверном уроке; теперь вы знаете её полный путь до trailers клиента.

Проверка: обычная ошибка из хендлера

Хендлер вернул errors.New("склад недоступен") без статуса. Что получит клиент?

Ретраи unary: что повторять и что нет#

Сеть сбоит мимолётно: сервер перезапускается, соединение дрейфует между машинами, балансировщик меняет адрес — и unary-вызов, который вчера прошёл, сегодня падает с codes.Unavailable. Повторить его руками вы уже умеете — ветка Unavailable в клиентском switch из прошлого урока прямо просится стать повтором. Но у grpc-go есть собственный механизм: клиент умеет повторять неудачные попытки сам, если для метода задана retryPolicy в service config — конфигурации канала в JSON или через API. Поля политики: maxAttempts — потолок попыток, включая первую (не меньше двух); initialBackoff, maxBackoff и backoffMultiplier — экспоненциальная задержка с жеребьёвкой; retryableStatusCodes — набор кодов, достойных повтора, в строковом виде вроде "unavailable". Потолок попыток на весь канал — пять по умолчанию (константа defaultMaxCallAttempts, настраивается опцией WithMaxCallAttempts), исчерпание возвращает ошибку с текстом «max retries exhausted». И здесь упираемся в главное слово unary-повторов — идемпотентность. GetOrder повторять безопасно: чтение, хоть сто раз, ответ один. CreateOrder — нельзя: каждая попытка создаёт заказ, и ретрай превратит «временный сбой сети» в «двойной заказ у клиента». Ссылки на идемпотентность в политике повтора, фильтры необратимых операций и service config целиком — материал отдельного урока про ретраи; сегодня достаточно усвоить правило: повтор включают не для всех методов подряд, а для тех, у которых повтор безопасен.

Проверка: код, достойный ретрая

Какой код чаще всего включают в retryableStatusCodes?

Чек-лист unary-вызова#

  • Ровно одно сообщение в каждую сторону: лишний ответ клиент засчитает как cardinality violation и вернёт codes.Internal.
  • Дедлайн на каждый вызов: context.WithTimeout + defer cancel(); тот же срок уезжает в заголовок grpc-timeout.
  • Сервер уважает дедлайн: долгая работа — порциями, между ними проверка ctx.Done(), иначе ресурсы горят впустую.
  • Наружу из хендлера — только status.Error/status.Errorf: обычная ошибка приедет клиентом как codes.Unknown.
  • Клиент ветвится по status.Code: NotFound, InvalidArgument, Unavailable и DeadlineExceeded — это разные реакции.
  • Повтор — только для идемпотентных методов: GetOrder можно, CreateOrder нельзя.

Соберите статусный возврат и разбор кода

Восстановите фрагмент: статусный возврат на сервере, таймаут и ветвление по кодам на клиенте.

// Сервер: отсутствующий заказ — нормальная ситуация, не сбой. if _, ok := s.orders[req.Id]; !ok { return nil, .Errorf(codes., "заказ %d не найден", req.Id) } // Клиент: срок на вызов и разбор кода. ctx, cancel := context.(context.Background(), 2*time.Second) defer cancel() order, err := client.GetOrder(ctx, &pb.GetOrderRequest{Id: 42}) if err != nil { switch .Code(err) { case codes.: fmt.Println("не успели за дедлайн") case codes.: fmt.Println("сервис недоступен, повторите позже") default: log.Fatalf("вызов не удался: %v", err) } return }

Путь unary-вызова на уровне транспорта

Расставьте по порядку события вызова GetOrder — от установки дедлайна до возврата из Invoke, включая кадры и trailers.

Строки

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

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

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

Одно честное замечание про формат: у самоучителя есть песочница с интерпретатором Go, но сеть в браузере недоступна — поднять сервер и постучаться в него оттуда невозможно, поэтому runner-ов в этом уроке и во всём gRPC-курсе нет. Всё, что здесь разобрано, проверяется локально за минуты: сервер из урока про первый gRPC-сервер, клиент из урока про gRPC-клиент, команда go run — и unary-вызов вживую: попробуйте послать grpc-timeout на две секунды в хендлер, который спит пять. Интерактивы (quiz, predict, fill, order, flashcards) — для закрепления теории. Дальше по маршруту: седьмой урок — server streaming, где WatchOrders из нашей схемы перестанет быть заглушкой Unimplemented; следом — client streaming и bidi, и таблица из этого урока оживёт строка за строкой. Дедлайны в глубину — распространение по цепочке сервисов и WithDeadline — в уроке про дедлайны и отмены; статус-коды с деталями и WithDetails — в уроке про обработку ошибок; ретраи с service config — в уроке про повторы. Полный маршрут курса — на странице gRPC-Go.

Карточки: unary под микроскопом

Термин

Unary (запрос-ответ)

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

Один Request → один Response, строго 1:1: лишнее сообщение клиент встречает кодом Internal («cardinality violation»). Внутри Invoke: SendMsg → CloseSend → RecvMsg → второй RecvMsg за trailers.

1 / 6

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

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

Вывод

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

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

  • Unary — модель запрос-ответ: ровно одно сообщение в каждую сторону; в схеме заказов это CreateOrder и GetOrder, а WatchOrders — server streaming из седьмого урока.
  • Типы вызовов различаются словом stream в .proto: без него — unary; сгенерированный Go-метод возвращает значение (Invoke) или стриминговый клиент: ServerStreamingClient, ClientStreamingClient, BidiStreamingClient.
  • Unary-вызов на HTTP/2: HEADERS с :path и grpc-timeout, DATA с 5-байтовым префиксом и protobuf, END_STREAM; ответ — заголовки, одно сообщение и trailers с grpc-status/grpc-message.
  • grpc-timeout шлётся всегда: две секунды — «2S», 250 миллисекунд — «250m»; сервер видит дедлайн в ctx хендлера и обязан проверять ctx.Done() в долгой работе.
  • Клиент не ждёт дольше дедлайна: Invoke вернёт DeadlineExceeded, даже если сервер ответит позже; Canceled — явная отмена, а не просрочка.
  • Хендлер возвращает статусные ошибки через status.Error/status.Errorf: обычная ошибка приедет клиентом как codes.Unknown; обёртка %w сохраняет код, но заменяет текст.
  • Ретраи настраиваются service config'ом: retryableStatusCodes (обычно Unavailable), экспоненциальный бэкофф, потолок 5 попыток; повторять безопасно только идемпотентные методы.

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

Поднимите сервер из урока 04 и клиент из 05, затем три эксперимента. Первый: добавьте в GetOrder time.Sleep(5 * time.Second) и убедитесь, что клиент с таймаутом в 2 секунды получит DeadlineExceeded, а лог сервера покажет, что хендлер доработал впустую; затем добавьте проверку ctx.Done() и сравните. Второй: замените status.Errorf в ветке NotFound на fmt.Errorf и прочитайте на клиенте codes.Unknown; верните код обёрткой %w и посмотрите, что изменилось. Третий: напишите service config с retryPolicy для GetOrder (retryableStatusCodes: "unavailable"), остановите сервер и проверьте, как клиент дожидается его перезапуска в пределах пяти попыток.

Личные заметкиПусто
Хранятся только в вашем браузере (gRPC · Типы вызовов: unary)

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

Чем unary отличается от стриминга на практике?

Unary — один запрос и один ответ: вызов выглядит как обычная функция. Стриминговые виды шлют поток сообщений в одном или обоих направлениях. Unary — дефолт; стриминг берут для неограниченных списков, событий в реальном времени и передачи больших данных по частям.

Что получит клиент, если хендлер вернул обычную ошибку?

codes.Unknown с текстом ошибки в grpc-message. Чтобы клиент мог ветвиться по коду, возвращайте из хендлера статусные ошибки — status.Error или status.Errorf; обёртка fmt.Errorf с %w поверх статуса сохранит код, но подменит сообщение.

Куда едет итоговый статус вызова?

В HTTP/2-trailers после тела ответа: grpc-status — номер кода десятичной строкой, grpc-message — сообщение в percent-кодировке; успешный вызов — grpc-status 0. Поэтому обратные прокси на пути gRPC-трафика обязаны поддерживать trailers.

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

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

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

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

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

Connect25 уроков~12 чпродвинутый25 уроков уже доступны

Connect: современный RPC от Buf

Курс Connect (Buf): протоколы, клиенты, ошибки, interceptors, браузерные клиенты, buf CLI и BSR. 25 уроков и итоговый проект API.

  • Строить сервисы на connectrpc.com/connect
  • Понимать три протокола и выбирать правильный
  • Настраивать buf: lint, breaking changes, generate
  • Писать interceptors и обрабатывать ошибки