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

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

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

Урок 4 из 25 ~20 мин +50 XP

Первый gRPC-сервер

Показательный grpc hello world из первого урока умещался в двенадцать строк main.go — этого хватает, чтобы понять механику, и не хватает, чтобы жить в продакшене. Здесь мы соберём настоящий grpc сервер Go: структура проекта cmd/internal/gen, реализация с зависимостями в полях, регистрация сервиса, net.Listen с осознанным выбором адреса и graceful shutdown по SIGTERM — всё на схеме заказов из уроков 02 и 03.

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

Ситуация типичная: схема заказов описана, код сгенерирован, интерфейс 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 отвечает за жизненный цикл — флаги, порт, сигналы, — а бизнес-логика живёт отдельно. Тогда хендлеры можно протестировать без сети и поднять второй бинарник с той же реализацией и другим портом.

Структура каталога

GO
shop/
├── 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 читается с ощущением подвоха.

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
KeepaliveParamspings и таймауты удержания соединенийурок 22

cmd/server/main.go: запуск и регистрация

GO
package 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#

АдресЧто означаетКогда брать
:50051TCP на всех интерфейсах, порт 50051сервер, к которому ходят по сети
localhost:50051TCP только на loopbackлокальная разработка: наружу не торчит
0.0.0.0:50051TCP на всех интерфейсах IPv4явная запись того же, что и ":"
:0TCP, случайный свободный порттесты: фактический порт вернёт lis.Addr()
"unix" + путьunix-доменный сокет вместо TCPIPC одной машины: 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 с корректным завершением

GO
package 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 в горутине, ожидание и остановка.

ctx, := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() errCh := make(chan error, 1) go func() { errCh <- s.(lis) }() select { case err := <-errCh: if err != nil { log.Fatalf("сервер завершился с ошибкой: %v", err) } case <-ctx.(): log.Println("сигнал завершения: останавливаю сервер") } done := make(chan struct{}) go func() { s.(); close(done) }()

Жизненный цикл сервера: соберите по порядку

Расставьте шаги полного жизненного цикла gRPC-сервера — от старта до финального предохранителя.

Строки

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

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

Карточки: скелет gRPC-сервера

Термин

grpc.NewServer

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

Конструктор сервера без сервисов и без сетевой активности. Настройка — через ServerOption: MaxRecvMsgSize (по умолчанию 4 МБ), MaxConcurrentStreams, ChainUnaryInterceptor, Creds, KeepaliveParams.

1 / 6

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

Сервер собран и корректно завершается, но пока к нему некому ходить: клиенты обычно ставят рядом с 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 · Первый gRPC-сервер)

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

Какой порт выбрать для gRPC-сервера?

50051 — конвенция из документации и примеров grpc-go, её удобно держать как дефолт флага. Важнее другое: слушайте все интерфейсы только там, где клиенту нужен сетевой доступ, а для локальной разработки берите localhost:порт.

Нужен ли mutex в реализации gRPC-сервиса?

Да, если хендлеры трогают общее состояние: grpc-go выполняет каждый вызов в своей горутине. Карта заказов без мьютекса — гонка данных, которую сразу покажет go run -race.

Чем GracefulStop отличается от Stop?

GracefulStop перестаёт принимать новые вызовы и блокируется, пока не закончатся идущие; Stop закрывает соединения немедленно и отменяет активные RPC. Стандартный приём — GracefulStop под таймаутом и Stop как предохранитель.

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

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

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

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

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

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

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

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

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