Содержание урока · 15 разделов
- Проект grpc сервера: cmd, internal и gen
- Реализация сервиса: зависимости в полях
- Контекст, указатели и конкурентные хендлеры
- grpc.NewServer: пустой, но с ручками
- Опции grpc.NewServer, которые пригодятся раньше прочих
- Регистрация: RegisterOrderServiceServer изнутри
- net.Listen: интерфейсы, порты и сокеты
- Формы адреса для net.Listen
- Serve: блокирующий цикл приёма
- Graceful shutdown: зачем сворачиваться медленно
- GracefulStop и Stop: разница на пальцах
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Ситуация типичная: схема заказов описана, код сгенерирован, интерфейс OrderServiceServer ждёт реализации. Проблема в том, что между «работает» и «работает годами» лежит куча решений, которые grpc hello world из первого урока не показывает: где лежат файлы сервера, куда девать зависимости хендлеров, что делать с приходящим от оркестратора SIGTERM. Собрать grpc сервер Go по-взрослому — значит ответить на них заранее, а не после первого инцидента. В этом уроке пройдём весь скелет: структура каталога, реализация OrderServiceServer с мьютексом и конструктором, анатомия grpc.NewServer с опциями, регистрация сервиса и что случится при дубликате, формы адреса для net.Listen — вплоть до unix-сокетов, и корректное завершение через GracefulStop с предохранителем. Каждый шаг опирается на уже готовые куски: схему заказов мы писали во втором уроке, генерацию делали в третьем — здесь только серверная половина уравнения.
Проект grpc сервера: cmd, internal и gen#
Начнём с раскладки, потому что она диктует, что где импортируется. Схема живёт в api/shop/v1/order.proto — единственный источник правды, который вы правите руками. Сгенерированный код — в gen/shopv1/: два файла из урока про кодогенерацию, которые мы не трогаем никогда. Хендлеры — в internal/server/order.go: папка internal имеет в Go особый статус, её могут импортировать только пакеты этого же модуля, и для кода, который не предназначен внешним пользователям библиотеки, это ровно то, что нужно. Точка входа — cmd/server/main.go: имена пакетов вида cmd/<бинарь> — устоявшаяся конвенция, по которой new коллега без объяснений понимает, что здесь собирается исполняемый файл. Главный принцип раскладки: main отвечает за жизненный цикл — флаги, порт, сигналы, — а бизнес-логика живёт отдельно. Тогда хендлеры можно протестировать без сети и поднять второй бинарник с той же реализацией и другим портом.
Структура каталога
GOshop/
├── api/
│ └── shop/
│ └── v1/
│ └── order.proto // контракт: правим только здесь
├── gen/
│ └── shopv1/
│ ├── order.pb.go // типы и сериализация (protoc-gen-go)
│ └── order_grpc.pb.go // интерфейс, клиент, регистратор (protoc-gen-go-grpc)
├── internal/
│ └── server/
│ └── order.go // наша реализация OrderServiceServer
├── cmd/
│ └── server/
│ └── main.go // запуск: флаги, listen, регистрация, Serve
├── go.mod
└── go.sumЧетыре папки, каждая со своей ролью: api — источник, gen — сгенерированный код, internal/server — реализации сервисов, cmd/server — точка входа. Команда запуска при такой раскладке: go run ./cmd/server.
Реализация сервиса: зависимости в полях#
Хендлеры — это методы вашей структуры, и самый важный вопрос её проектирования звучит так: где живут зависимости? В хранилище, логгере, конфиге — во всём, что нужно методам. Ответ «в глобальных переменных» превратит пакет в нечто несобираемое в тестах, поэтому зависимости кладут в поля структуры, а не в пакетные переменные. Наш учебный хранилище — карта заказов с мьютексом и счётчиком идентификаторов; в боевом сервисе на этом месте был бы интерфейс репозитория. Второе решение — встраивание UnimplementedOrderServiceServer. Мы разбирали механику в уроке про кодогенерацию, здесь виден практический эффект: мы реализовали CreateOrder и GetOrder, а WatchOrders из схемы — нет, и сервер всё равно собирается. Клиент, позвавший стриминг-метод раньше времени, получит честный ответ codes.Unimplemented с сообщением «method WatchOrders not implemented» — ровно так выглядят заглушки в сгенерированном файле. Реализуем WatchOrders в уроке про server streaming, и до тех пор контракт уже работает — просто один метод отказывает вежливо.
internal/server/order.go: хендлеры CreateOrder и GetOrder
GO// Пакет internal/server — реализации gRPC-сервисов.
package server
import (
"context"
"sync"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
pb "github.com/example/shop/gen/shopv1"
)
// OrderServiceServer реализует сервис заказов из order.proto.
// Зависимости — в полях, а не в глобальных переменных.
type OrderServiceServer struct {
// Встраивание значением: Unimplemented-заглушки закрывают
// методы, которые мы ещё не реализовали (WatchOrders).
pb.UnimplementedOrderServiceServer
mu sync.Mutex // хендлеры идут конкурентно
orders map[int64]*pb.Order // учебное хранилище
nextID int64
}
func NewOrderServiceServer() *OrderServiceServer {
return &OrderServiceServer{
orders: make(map[int64]*pb.Order),
}
}
func (s *OrderServiceServer) CreateOrder(ctx context.Context, req *pb.CreateOrderRequest) (*pb.CreateOrderReply, error) {
if len(req.Items) == 0 {
return nil, status.Error(codes.InvalidArgument, "заказ без позиций не принимаем")
}
s.mu.Lock()
s.nextID++
id := s.nextID
s.orders[id] = &pb.Order{
Id: id,
Items: req.Items,
Status: pb.OrderStatus_ORDER_STATUS_NEW,
CreatedAt: timestamppb.Now(),
}
s.mu.Unlock()
return &pb.CreateOrderReply{Id: id}, nil
}
func (s *OrderServiceServer) GetOrder(ctx context.Context, req *pb.GetOrderRequest) (*pb.Order, error) {
s.mu.Lock()
defer s.mu.Unlock()
order, ok := s.orders[req.Id]
if !ok {
// Честный способ сказать клиенту «не найдено» кодом,
// а не строкой; полный разбор статус-кодов — в уроке 13.
return nil, status.Errorf(codes.NotFound, "заказ %d не найден", req.Id)
}
return order, nil
}Читаем по строкам: структура встраивает Unimplemented значением (почему не указателем — callout ниже); mu защищает карту, потому что gRPC вызывает хендлеры конкурентно; CreateOrder валидирует запрос, резервирует идентификатор и штампует CreatedAt через timestamppb — тот самый Timestamp из схемы; GetOrder на misses возвращает статус NotFound. Обратите внимание: байты protobuf вы нигде не видите — фреймворк уже декодировал запрос в готовую структуру до вызова метода.
Контекст, указатели и конкурентные хендлеры#
Три правила сигнатур, которые стоит выучить до написания первого хендлера. Первое: context.Context идёт первым аргументом — по нему ходят дедлайны и отмена, это единственный канал, по которому клиент сообщает серверу «больше не жди». Контекст принадлежит конкретному вызову, поэтому хранить его в полях структуры нельзя: положили ctx от первого запроса — и все следующие ответы заканчиваются по чужому таймауту. Второе: вход и выход — указатели на сгенерированные структуры (pb.CreateOrderRequest, pb.CreateOrderReply); возвращать nil вместе с ошибкой нормально, но nil без ошибки — баг, клиент получит пустое сообщение. Третье, самое недооценённое: хендлеры выполняются конкурентно. Внутри grpc-go на каждое соединение и на каждый вызов заводится своя горутина — два клиента могут позвать CreateOrder в один и тот же момент, и общий счётчик nextID без мьютекса поплывёт. Если горутины пока в тумане, начните с урока про горутины: без этой модели конкурентности gRPC читается с ощущением подвоха.
Строка pb.UnimplementedOrderServiceServer внутри структуры — встраивание значением. Если встроить указателем и не инициализировать, первый же вызов нереализованного метода упадёт в nil pointer dereference. Свежие версии генератора ловят это на старте: сгенерированная Register-функция проверяет встраивание через testEmbeddedByValue и паникует при регистрации, а не когда-нибудь потом. Правило одно: встраивайте значением — и проблемы нет.
grpc.NewServer: пустой, но с ручками#
grpc.NewServer(opt ...ServerOption) — конструктор с вариадиком опций. Его документация формулирует честно: создаётся сервер, у которого не зарегистрировано ни одного сервиса и который ещё не начал принимать запросы. Вся настройка идёт через ServerOption — функции, возвращающие ServerOption; это паттерн функциональных опций, и он означает, что настройки читаются один раз при создании, а не роятся сеттерами по коду. Какие ручки круят чаще прочих: MaxRecvMsgSize задаёт потолок входящего сообщения — по умолчанию 4 мегабайта, и первый же клиент, заливший кадр побольше, получит ResourceExhausted; MaxConcurrentStreams ограничивает число стримов на соединении; ChainUnaryInterceptor вешает цепочку перехватчиков — аналог middleware, им посвящён отдельный урок; Creds подключает TLS; KeepaliveParams настраивает удержание соединений. Мой совет для старта: создавайте сервер с пустым NewServer() и добавляйте опции, когда почувствуете боль, — каждая из них требует понимания последствий, которые мы разберём по ходу курса.
Опции grpc.NewServer, которые пригодятся раньше прочих#
| Опция | Что задаёт | Разбор в курсе |
|---|---|---|
| MaxRecvMsgSize | потолок входящего сообщения, по умолчанию 4 МБ | урок 22 |
| MaxSendMsgSize | потолок исходящего сообщения | урок 22 |
| MaxConcurrentStreams | сколько стримов обслуживать на одном соединении | урок 22 |
| ChainUnaryInterceptor | цепочка перехватчиков для unary-вызовов | урок 11 |
| Creds | транспортные креденшелы: TLS и mTLS | урок 12 |
| KeepaliveParams | pings и таймауты удержания соединений | урок 22 |
cmd/server/main.go: запуск и регистрация
GOpackage main
import (
"flag"
"fmt"
"log"
"net"
"google.golang.org/grpc"
pb "github.com/example/shop/gen/shopv1"
"github.com/example/shop/internal/server"
)
func main() {
// Порт выносим во флаг: один бинарник работает и на 50051
// локально, и на любом порту, который даст оркестратор.
port := flag.Int("port", 50051, "порт gRPC-сервера")
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("не удалось открыть порт %d: %v", *port, err)
}
s := grpc.NewServer()
// Регистрация — и только ДО Serve.
pb.RegisterOrderServiceServer(s, server.NewOrderServiceServer())
log.Printf("grpc сервер слушает %s", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}Скелет — четыре действия: net.Listen открывает порт, grpc.NewServer создаёт сервер, pb.RegisterOrderServiceServer привязывает реализацию из internal/server, s.Serve запускает приём соединений. Порядок критичен: регистрация обязана случиться до Serve, иначе процесс умрёт на старте с сообщением про RegisterService after Server.Serve. lis.Addr() печатает фактический адрес — это пригодится, когда порт назначит ОС.
Регистрация: RegisterOrderServiceServer изнутри#
Register-функция выглядит как магия, но внутри — три строки. Сгенерированный код даёт функцию RegisterOrderServiceServer(s grpc.ServiceRegistrar, srv OrderServiceServer), которая зовёт s.RegisterService(&OrderService_ServiceDesc, srv). Интерфейс ServiceRegistrar содержит единственный метод RegisterService(grpc.ServiceDesc, any) — ему удовлетворяет grpc.Server, поэтому функция принимает и наш сервер, и любой другой регистратор, который пригодится в тестах. Сердце регистрации — дескриптор OrderService_ServiceDesc: ServiceName со строкой "shop.v1.OrderService" (пакет плюс имя сервиса — тот самый путь HTTP-вызова из урока про синтаксис), HandlerType с nil-указателем на интерфейс для проверки типов, списки Methods и Streams с хендлерами каждого метода и Metadata с путём к исходному .proto. На один процесс можно зарегистрировать сколько угодно сервисов — health-check регистрируют вторым сервисом почти всегда. Но есть жёсткие правила: дубликат с тем же ServiceName убивает процесс сообщением «found duplicate service registration», а вызов после старта — «RegisterService after Server.Serve». Оба — logger.Fatalf, то есть не паника, которую можно recover-нуть, а немедленный выход: код настраивают так, чтобы эти ситуации были физически невозможны.
Проверка: когда регистрировать
Позже какого момента нельзя вызывать pb.RegisterOrderServiceServer?
net.Listen: интерфейсы, порты и сокеты#
net.Listen(network, address) открывает сокет, и его первый аргумент — «tcp» — не формальность: сеть можно поменять. Адресом управлять интереснее. Строка ":50051" значит «все интерфейсы, порт 50051»: сервер доступен снаружи, и в контейнере это то, что нужно. Строка "localhost:50051" слушает только loopback — снаружи не достучаться, и для локальной разработки это безопаснее: сервис торчит ровно там, где вы его позвали. Порт "0" просит ОС выдать случайный свободный — фактический адрес вернёт lis.Addr(), и это любимый приём интеграционных тестов. Сеть "unix" открывает unix-доменный сокет вместо TCP: так общаются процессы одной машины, например runtime контейнеров с kubelet'ом. Типовая ошибка старта: «listen tcp :50051: bind: address already in use» — порт занят прошлым запуском, который не завершился; ищите виновника и убивайте осознанно. Нюанс, о котором стоит знать заранее: рантайм Go для всех принятых соединений переопределяет системные настройки TCP keepalive и держит их 15 секунд — на те случаи, когда наладка keepalive на нижнем уровне покажется странной.
Формы адреса для net.Listen#
| Адрес | Что означает | Когда брать |
|---|---|---|
| :50051 | TCP на всех интерфейсах, порт 50051 | сервер, к которому ходят по сети |
| localhost:50051 | TCP только на loopback | локальная разработка: наружу не торчит |
| 0.0.0.0:50051 | TCP на всех интерфейсах IPv4 | явная запись того же, что и ":" |
| :0 | TCP, случайный свободный порт | тесты: фактический порт вернёт lis.Addr() |
| "unix" + путь | unix-доменный сокет вместо TCP | IPC одной машины: sidecar, k8s CRI |
Serve: блокирующий цикл приёма#
s.Serve(lis) — точка, после которой сервер живёт: метод принимает соединения и не возвращается, пока цикл Accept не упадёт с фатальной ошибкой. Слушатель закрывается при возврате — рукопожатие с net.Listen у Serve полное: кто открыл, тот закрыт самим Serve. Семантика возвращаемого значения — половина всех недоразумений: Serve вернёт nil, если сервер остановили Stop или GracefulStop, и не-nil ошибку во всех остальных случаях. Отдельная ветка: вызвать Serve на уже остановленном сервере — получите ErrServerStopped сразу. Поэтому log.Fatalf вокруг Serve в примерах — не лень автора: не-nil из Serve означает аварийное падение приёма, и продолжать процесс смысла нет. Но обратите внимание, чего в этом main.go нет: реакции на Ctrl+C. Ctrl+C придёт, процесс умрёт по умолчанию, все идущие вызовы оборвутся на середине — клиенты увидят ошибку соединения вместо ответа. Для дев-прогона нормально, для продакшена — нет; этим и займёмся дальше.
Проверка: что вернёт Serve
Сервер остановили через s.GracefulStop(). Что вернёт завершившийся s.Serve(lis)?
Graceful shutdown: зачем сворачиваться медленно#
Оркестратор снимает под не SIGINT, а SIGTERM: Kubernetes шлёт его в контейнер и ждёт terminationGracePeriodSeconds, обычно тридцать секунд. Поведение Go-процесса по умолчанию на любой из этих сигналов — немедленная смерть, и для сервера это означает: соединения рвутся, идущие RPC не дорабатывают, клиент получает ошибку. Ровно те вызовы, которые занимали секунду, падают, потому что деплой занял мгновение. Правильное завершение выглядит так: перестать принимать новые запросы, доработать те, что уже в полёте, освободить ресурсы, закрыться. Стандартная библиотека даёт для этого signal.NotifyContext — функцию, которая превращает SIGINT и SIGTERM в отмену контекста, то есть в обычный для Go event: <-ctx.Done(). Дальше начинается работа gRPC-сервера — GracefulStop, и с ним есть один известный подводный камень, о котором — после кода.
main.go с корректным завершением
GOpackage main
import (
"context"
"flag"
"fmt"
"log"
"net"
"os"
"os/signal"
"syscall"
"time"
"google.golang.org/grpc"
pb "github.com/example/shop/gen/shopv1"
"github.com/example/shop/internal/server"
)
func main() {
port := flag.Int("port", 50051, "порт gRPC-сервера")
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("не удалось открыть порт %d: %v", *port, err)
}
s := grpc.NewServer()
pb.RegisterOrderServiceServer(s, server.NewOrderServiceServer())
log.Printf("grpc сервер слушает %s", lis.Addr())
// SIGINT (Ctrl+C) и SIGTERM превращаются в отмену контекста.
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
// Serve блокируется до остановки — выносим в горутину.
errCh := make(chan error, 1)
go func() { errCh <- s.Serve(lis) }()
select {
case err := <-errCh:
// Сервер умер сам, сигнала не дождались.
if err != nil {
log.Fatalf("сервер завершился с ошибкой: %v", err)
}
case <-ctx.Done():
log.Println("сигнал завершения: останавливаю сервер")
}
// Предохранитель: 10 секунд на добработку идущих вызовов,
// иначе — жёсткий Stop (паттерн из примеров grpc-go).
timer := time.AfterFunc(10*time.Second, func() {
log.Println("не уложились в 10 секунд: force stop")
s.Stop()
})
defer timer.Stop()
s.GracefulStop() // доработать идущие RPC и вернуться
log.Println("сервер остановлен")
}Читаем по шагам: signal.NotifyContext держит контекст до прихода SIGINT/SIGTERM; Serve ушёл в горутину, потому что блокирует; select ждёт либо ошибку сервера, либо сигнал. Получив сигнал, мы заводим предохранитель — time.AfterFunc через десять секунд зовёт s.Stop() — и вызываем s.GracefulStop(). Если идущие вызовы закончились раньше, GracefulStop вернётся сам и таймер мы отменим defer-ом; если какой-то стрим висит вечно, через десять секунд Stop оборвёт его принудительно. Именно так выглядит graceful stop в официальных примерах grpc-go.
GracefulStop и Stop: разница на пальцах#
Два метода остановки — и разница между ними ровно одна, но решающая. GracefulStop перестаёт принимать новые соединения и вызовы, а затем блокируется, пока не закончатся все идущие RPC: клиенты дорабатывают, никто не брошен. Stop закрывает соединения и слушателей немедленно, активные вызовы отменяются на месте, клиенты узнают об этом ошибкой соединения. После любого из них Serve возвращает nil — поэтому errCh-паттерн из кода выше не падает. Оба метода безопасно вызывать и на уже остановленном сервере: повторный вызов просто вернётся. Теперь обещанный подводный камень: GracefulStop блокируется до конца всех идущих RPC, а значит, вечнодвижущийся стрим — тот же WatchOrders, из которого клиент не уходит, — никогда не даст GracefulStop вернуться. Процесс зависнет на этапе завершения, оркестратор подождёт свой grace-период и пришлёт SIGKILL. Поэтому предохранитель с Stop по таймауту — не опция, а обязательная часть схемы: десять секунд из примера — разумный дефолт, подгоняйте под свои самые долгие легитимные вызовы.
Что напечатает завершение?
Прочитайте код и выберите, что он выведет:
// Сервер поднят: NewServer, Register, Serve в горутине.
// Пришёл SIGTERM, select вышел в ветку сигнала.
log.Println("сигнал завершения: останавливаю сервер")
timer := time.AfterFunc(10*time.Second, func() { s.Stop() })
defer timer.Stop()
s.GracefulStop() // в этот момент идут два долгих unary-вызова,
// они успевают завершиться за 2 секунды
log.Println("сервер остановлен")Соберите схему завершения
Восстановите фрагмент graceful shutdown: преобразование сигнала в контекст, Serve в горутине, ожидание и остановка.
Жизненный цикл сервера: соберите по порядку
Расставьте шаги полного жизненного цикла gRPC-сервера — от старта до финального предохранителя.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: скелет gRPC-сервера
Что дальше по курсу#
Сервер собран и корректно завершается, но пока к нему некому ходить: клиенты обычно ставят рядом с grpcurl (отладка без кода — отдельный урок про reflection, а пока запомните слово). Дальше по курсу — клиент: grpc.NewClient, первый вызов и обработка ошибки в уроке 05, тонкости unary — таймауты и простые ошибки — в уроке 06. WatchOrders остался заглушкой Unimplemented — честный контракту ответ «не поддерживается»; реализуем его в уроке про server streaming, а цепочку перехватчиков — в уроке про interceptors. Если хочется вернуться и освежить, откуда взялись типы и регистратор — это кодогенерация из урока 03, а сам контракт заказов — схема proto3 из урока 02. Про конкурентную модель, на которой держатся хендлеры, подробно рассказывают горутины в продвинутом курсе; полный маршрут курса — на странице gRPC-Go.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Скелет сервера — четыре шага: net.Listen открывает порт, grpc.NewServer создаёт пустой сервер, RegisterOrderServiceServer привязывает реализацию, s.Serve(lis) запускает блокирующий цикл приёма.
- Реализация — своя структура с зависимостями в полях и встроенным Unimplemented значением: нереализованные методы отвечают Unimplemented, а не ломают компиляцию.
- Хендлеры идут конкурентно — по горутине на каждый вызов, — поэтому общее состояние защищайте мьютексом, а ctx не храните в полях.
- Регистрация только до Serve: дубликат ServiceName или вызов после старта убивают процесс через logger.Fatalf.
- Адрес «:50051» — все интерфейсы; «localhost:50051» — loopback; «:0» — случайный порт для тестов; сеть «unix» — сокет для IPC одной машины.
- Serve возвращает nil после Stop/GracefulStop и не-nil ошибку при падении Accept; ErrServerStopped — ответ на Serve уже остановленного сервера.
- GracefulStop дожидается идущих RPC; вечный стрим не даёт ему вернуться — обязателен предохранитель Stop по таймауту, как в официальных примерах grpc-go.
Домашнее задание
Соберите проект по раскладке урока: схема orders из урока 02 в api/shop/v1, генерация в gen/shopv1, реализация CreateOrder и GetOrder в internal/server, main.go в cmd/server с флагом --port и graceful shutdown с предохранителем на 5 секунд. Затем умышленно вызовите RegisterOrderServiceServer дважды и посмотрите, как процесс умирает с сообщением про duplicate service registration, — после этого проверьте запуск через go run ./cmd/server --port=50052.
Личные заметкиПусто
Вопросы и ответы по уроку
Какой порт выбрать для gRPC-сервера?
50051 — конвенция из документации и примеров grpc-go, её удобно держать как дефолт флага. Важнее другое: слушайте все интерфейсы только там, где клиенту нужен сетевой доступ, а для локальной разработки берите localhost:порт.
Нужен ли mutex в реализации gRPC-сервиса?
Да, если хендлеры трогают общее состояние: grpc-go выполняет каждый вызов в своей горутине. Карта заказов без мьютекса — гонка данных, которую сразу покажет go run -race.
Чем GracefulStop отличается от Stop?
GracefulStop перестаёт принимать новые вызовы и блокируется, пока не закончатся идущие; Stop закрывает соединения немедленно и отменяет активные RPC. Стандартный приём — GracefulStop под таймаутом и Stop как предохранитель.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Connect: современный RPC от Buf
Курс Connect (Buf): протоколы, клиенты, ошибки, interceptors, браузерные клиенты, buf CLI и BSR. 25 уроков и итоговый проект API.
- Строить сервисы на connectrpc.com/connect
- Понимать три протокола и выбирать правильный
- Настраивать buf: lint, breaking changes, generate
- Писать interceptors и обрабатывать ошибки