---
title: "Первый gRPC-сервер"
description: "Первый grpc сервер Go: структура проекта, grpc.NewServer, регистрация сервиса, net.Listen и graceful shutdown — собираем сервер заказов с построчным разбором."
course: "gRPC-Go: высокопроизводительный RPC"
url: "https://golangmaster.ru/courses/grpc-go/lessons/04-server/"
published: "2026-09-19"
updated: "2026-09-26"
minutes: 20
---

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

*Курс «gRPC-Go: высокопроизводительный RPC», урок 4 из 25. ~20 минут. Обновлено 26.09.2026. Источник: https://golangmaster.ru/courses/grpc-go/lessons/04-server/*

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

Ситуация типичная: схема заказов описана, код сгенерирован, интерфейс OrderServiceServer ждёт реализации. Проблема в том, что между «работает» и «работает годами» лежит куча решений, которые [grpc hello world из первого урока](/courses/grpc-go/lessons/01-vvedenie/) не показывает: где лежат файлы сервера, куда девать зависимости хендлеров, что делать с приходящим от оркестратора 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 без мьютекса поплывёт. Если горутины пока в тумане, начните с [урока про горутины](/courses/go-advanced/lessons/04-gorutiny/): без этой модели конкурентности gRPC читается с ощущением подвоха.

> **Встраивайте Unimplemented значением, а не указателем.** Строка 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: запуск и регистрация

```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?

- После создания grpc.NewServer()
- ✅ После запуска s.Serve(lis)
- После net.Listen
- После первого вызова от клиента

Регистрация обязана произойти до Serve: внутренняя проверка замечает уже запущенный сервер и завершает процесс с «grpc: Server.RegisterService after Server.Serve». Вызовы net.Listen и grpc.NewServer порядка не ломают — важна только пара регистрация-до-Serve.

### 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)?

- Ошибку ErrServerStopped
- ✅ nil
- Панику
- Статус codes.Canceled

Документация формулирует прямо: Serve вернёт не-nil ошибку, если только не был вызван Stop или GracefulStop. После GracefulStop возвращает nil. ErrServerStopped — ответ на другую ситуацию: Serve вызвали на уже остановленном сервере.

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

### Что напечатает завершение?

```go
// Сервер поднят: 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("сервер остановлен")
```

**Что выведет программа?**

- ✅ «сервер остановлен», и программа завершается
- log.Fatalf: Serve вернул ошибку ErrServerStopped
- Программа зависнет навсегда: вызовы не дают остановиться
- Паника: повторный вызов GracefulStop

GracefulStop дождётся обоих идущих вызовов (они уложились в 2 секунды из 10) и вернётся, main напечатает финальную строку и завершится. Serve при этом вернёт nil — не-nil он даёт только при аварийном падении приёма, а ErrServerStopped — это ответ на Serve ПОСЛЕ остановки, не наоборот.

### Соберите схему завершения

Восстановите фрагмент graceful shutdown: преобразование сигнала в контекст, Serve в горутине, ожидание и остановка.

```go
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) }()
```

Подсказка: Сигналы в контекст превращает NotifyContext; принимать соединения начинает Serve; отмена контекста читается через ctx.Done(); доработать идущие вызовы просит GracefulStop.

Правильные ответы в пропусках: `NotifyContext`, `Serve`, `Done`, `GracefulStop`

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

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

Правильный порядок строк:

1. `flag.Parse() — прочитать порт и прочие настройки`
2. `net.Listen("tcp", ":50051") — открыть порт`
3. `grpc.NewServer() — создать сервер без сервисов`
4. `pb.RegisterOrderServiceServer(s, impl) — привязать реализацию`
5. `go func() { s.Serve(lis) }() — начать принимать соединения`
6. `<-ctx.Done() — дождаться SIGTERM/SIGINT`
7. `s.GracefulStop() — доработать идущие вызовы`
8. `s.Stop() — аварийный резерв по таймауту`

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

- **grpc.NewServer** — Конструктор сервера без сервисов и без сетевой активности. Настройка — через ServerOption: MaxRecvMsgSize (по умолчанию 4 МБ), MaxConcurrentStreams, ChainUnaryInterceptor, Creds, KeepaliveParams.
- **RegisterOrderServiceServer** — Сгенерированная функция регистрации: зовёт s.RegisterService(&OrderService_ServiceDesc, impl). Только до Serve; дубликат или вызов после старта убивают процесс (logger.Fatalf).
- **ServiceDesc** — Дескриптор сервиса: ServiceName «shop.v1.OrderService» — путь HTTP-вызова, HandlerType для проверки типов, списки Methods и Streams, Metadata с путём к .proto.
- **«:50051» vs «localhost:50051»** — Двоеточие — все интерфейсы, сервер виден по сети. localhost — только loopback, снаружи не достучаться. «:0» — случайный свободный порт, фактический вернёт lis.Addr().
- **Serve(lis)** — Блокирующий цикл приёма; закрывает слушатель при возврате. Возвращает nil после Stop/GracefulStop, не-nil ошибку — при падении Accept; ErrServerStopped — если Serve позвали после остановки.
- **GracefulStop vs Stop** — GracefulStop: не принимает новое, дожидается идущих RPC. Stop: рвёт соединения немедленно, активные вызовы отменяются. Вечный стрим не даёт GracefulStop вернуться — нужен предохранитель Stop по таймауту.

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

Сервер собран и корректно завершается, но пока к нему некому ходить: клиенты обычно ставят рядом с grpcurl (отладка без кода — отдельный урок про reflection, а пока запомните слово). Дальше по курсу — клиент: grpc.NewClient, первый вызов и обработка ошибки в уроке 05, тонкости unary — таймауты и простые ошибки — в уроке 06. WatchOrders остался заглушкой Unimplemented — честный контракту ответ «не поддерживается»; реализуем его в уроке про server streaming, а цепочку перехватчиков — в уроке про interceptors. Если хочется вернуться и освежить, откуда взялись типы и регистратор — это [кодогенерация из урока 03](/courses/grpc-go/lessons/03-generaciya/), а сам контракт заказов — [схема proto3 из урока 02](/courses/grpc-go/lessons/02-proto3/). Про конкурентную модель, на которой держатся хендлеры, подробно рассказывают [горутины в продвинутом курсе](/courses/go-advanced/lessons/04-gorutiny/); полный маршрут курса — на [странице gRPC-Go](/courses/grpc-go/).

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

- Скелет сервера — четыре шага: 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 как предохранитель.
