gRPC в Go: первый сервис и клиент на реальном примере
Пишем gRPC-сервис и клиент на Go с нуля — от .proto-файла до работающего вызова. Разбираем каждый шаг так, чтобы после прочтения у вас запускался рабочий проект, а не пустой шаблон.

Что понадобится до начала
gRPC — фреймворк удалённых вызовов от Google, который использует HTTP/2 и Protocol Buffers для сериализации. В отличие от REST с JSON, контракт описывается строго в .proto-файле, а код клиента и сервера генерируется автоматически. Результат — типизированный интерфейс, бинарный формат на проводе и встроенная поддержка стриминга.
В этом гайде мы соберём сервис управления задачами: создание задачи и получение списка. К концу у вас будет работающая пара сервер + клиент, которую можно расширять.
Убедитесь, что на машине установлено следующее:
- Go 1.22+ — проверьте командой
go version; protoc— компилятор Protocol Buffers (версия 25.x или свежее);- плагины для Go:
protoc-gen-goиprotoc-gen-go-grpc.
Установка плагинов:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
После установки убедитесь, что $(go env GOPATH)/bin есть в вашем PATH. Без этого protoc не найдёт плагины и выдаст ошибку protoc-gen-go: program not found.
Шаг 1. Структура проекта
Создайте директорию и инициализируйте модуль:
mkdir grpc-tasks && cd grpc-tasks
go mod init grpc-tasks
Финальная структура будет такой:
grpc-tasks/
├── proto/
│ └── tasks.proto
├── gen/
│ └── tasks/ # сгенерированный код
├── server/
│ └── main.go
├── client/
│ └── main.go
├── go.mod
└── go.sum
Проверка: команда go mod init прошла без ошибок, файл go.mod появился в корне.
Шаг 2. Описываем контракт в .proto
Создайте файл proto/tasks.proto:
syntax = "proto3";
package tasks;
option go_package = "grpc-tasks/gen/tasks";
// Запрос на создание задачи
message CreateTaskRequest {
string title = 1;
string description = 2;
}
// Ответ с созданной задачей
message CreateTaskResponse {
string id = 1;
string title = 2;
string description = 3;
}
// Пустой запрос для получения списка
message ListTasksRequest {}
// Список задач
message ListTasksResponse {
repeated CreateTaskResponse tasks = 1;
}
// Сервис управления задачами
service TaskService {
rpc CreateTask(CreateTaskRequest) returns (CreateTaskResponse);
rpc ListTasks(ListTasksRequest) returns (ListTasksResponse);
}
go_package указывает, куда генератор положит Go-файлы и какой пакет им присвоит. Если путь не совпадёт с реальной директорией, импорт в коде сломается.
Шаг 3. Генерируем Go-код
Запустите из корня проекта:
protoc \
--go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
proto/tasks.proto
Но файлы окажутся рядом с .proto. Чтобы они легли в gen/tasks/, измените команду:
mkdir -p gen/tasks
protoc \
--go_out=gen/tasks --go_opt=paths=source_relative \
--go-grpc_out=gen/tasks --go-grpc_opt=paths=source_relative \
proto/tasks.proto
Проверка: в gen/tasks/ появились два файла — tasks.pb.go (структуры сообщений) и tasks_grpc.pb.go (интерфейсы сервиса).
Что внутри сгенерированного кода
Файл tasks_grpc.pb.go содержит два ключевых элемента: интерфейс TaskServiceServer, который вы реализуете на сервере, и структуру TaskServiceClient, которую используете на клиенте. Править эти файлы руками нельзя — при следующей генерации изменения пропадут.
Шаг 4. Пишем сервер
Создайте server/main.go:
package main
import (
"context"
"log"
"net"
"sync"
"google.golang.org/grpc"
pb "grpc-tasks/gen/tasks"
"github.com/google/uuid"
)
// taskServer хранит задачи в памяти
type taskServer struct {
pb.UnimplementedTaskServiceServer
mu sync.Mutex
tasks []*pb.CreateTaskResponse
}
func (s *taskServer) CreateTask(
ctx context.Context,
req *pb.CreateTaskRequest,
) (*pb.CreateTaskResponse, error) {
s.mu.Lock()
defer s.mu.Unlock()
task := &pb.CreateTaskResponse{
Id: uuid.New().String(),
Title: req.GetTitle(),
Description: req.GetDescription(),
}
s.tasks = append(s.tasks, task)
log.Printf("создана задача: %s", task.GetId())
return task, nil
}
func (s *taskServer) ListTasks(
ctx context.Context,
req *pb.ListTasksRequest,
) (*pb.ListTasksResponse, error) {
s.mu.Lock()
defer s.mu.Unlock()
return &pb.ListTasksResponse{Tasks: s.tasks}, nil
}
func main() {
listener, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("не удалось занять порт: %v", err)
}
srv := grpc.NewServer()
pb.RegisterTaskServiceServer(srv, &taskServer{})
log.Println("сервер слушает :50051")
if err := srv.Serve(listener); err != nil {
log.Fatalf("ошибка сервера: %v", err)
}
}
Обратите внимание на UnimplementedTaskServiceServer — это встраиваемая структура, которая позволяет не реализовывать все методы сервиса сразу. Без неё проект не скомпилируется при добавлении нового RPC в .proto.
Шаг 5. Пишем клиент
Создайте client/main.go:
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "grpc-tasks/gen/tasks"
)
func main() {
conn, err := grpc.NewClient(
"localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalf("не удалось подключиться: %v", err)
}
defer conn.Close()
client := pb.NewTaskServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Создаём задачу
created, err := client.CreateTask(ctx, &pb.CreateTaskRequest{
Title: "Разобраться с gRPC",
Description: "Написать первый сервис и клиент",
})
if err != nil {
log.Fatalf("ошибка CreateTask: %v", err)
}
log.Printf("задача создана: id=%s title=%s", created.GetId(), created.GetTitle())
// Получаем список
list, err := client.ListTasks(ctx, &pb.ListTasksRequest{})
if err != nil {
log.Fatalf("ошибка ListTasks: %v", err)
}
for _, t := range list.GetTasks() {
log.Printf(" [%s] %s", t.GetId(), t.GetTitle())
}
}
Подтяните зависимости:
go mod tidy
Шаг 6. Запускаем и проверяем
- В первом терминале запустите сервер:
go run ./server. Должна появиться строкасервер слушает :50051. - Во втором терминале запустите клиент:
go run ./client. В выводе — ID созданной задачи и список из одного элемента. - Запустите клиент ещё раз — список вырастет до двух задач (данные живут в памяти серверного процесса).
Если всё прошло по плану, вы увидите примерно следующее:
2024/01/15 12:00:01 задача создана: id=a1b2c3... title=Разобраться с gRPC
2024/01/15 12:00:01 [a1b2c3...] Разобраться с gRPC
gRPC жёстко связывает клиент и сервер общим контрактом. Это главная сила и главное ограничение: интерфейс не получится «подправить на лету», как JSON-ответ в REST. Зато несоответствие типов ловится на этапе компиляции, а не в три часа ночи в логах.
gRPC vs REST: когда что выбирать
| Критерий | gRPC | REST + JSON |
|---|---|---|
| Формат данных | Protocol Buffers (бинарный) | JSON (текстовый) |
| Транспорт | HTTP/2 (мультиплексирование, стриминг) | HTTP/1.1 или HTTP/2 |
| Типизация контракта | Строгая, из .proto | Опциональная (OpenAPI) |
| Поддержка в браузере | Только через grpc-web прокси | Нативная |
| Человекочитаемость | Требует декодирования | Читается глазами и curl |
| Стриминг | Встроенный (server, client, bidirectional) | SSE, WebSocket — отдельно |
Если вам нужен публичный API для фронтенда в браузере — REST проще. Если сервисы общаются друг с другом внутри кластера, данных много, а типизация критична — gRPC выигрывает по скорости разработки и по размеру сообщений на проводе.
Если не получилось
protoc-gen-go: program not found
Плагин установлен, но protoc не видит его в PATH. Проверьте: which protoc-gen-go. Если пусто — добавьте export PATH="$PATH:$(go env GOPATH)/bin" в профиль оболочки и перезапустите терминал.
cannot find module providing package grpc-tasks/gen/tasks
Путь в go_package внутри .proto не совпадает с реальной директорией. Убедитесь, что генерация положила файлы именно в gen/tasks/, а модуль называется grpc-tasks (как в go.mod).
connection refused при запуске клиента
Сервер не запущен или слушает другой порт. Проверьте, что в первом терминале есть строка сервер слушает :50051, и что порт не занят другим процессом (lsof -i :50051).
undefined: pb.UnimplementedTaskServiceServer
Версии protoc-gen-go-grpc и библиотеки google.golang.org/grpc несовместимы. Запустите go get -u google.golang.org/grpc и перегенерируйте код.
rpc error: code = Unimplemented
Метод описан в .proto, но не реализован в серверной структуре. Убедитесь, что имя метода совпадает с тем, что в сгенерированном интерфейсе, включая регистр.
Что дальше
Рабочий пример — точка старта, а не финиш. Вот конкретные следующие шаги:
- Добавьте TLS — в продакшене
insecure.NewCredentials()недопустим. Документация gRPC-Go описывает настройку черезcredentials.NewServerTLSFromFile. - Попробуйте server streaming — измените
ListTasksтак, чтобы задачи отдавались потоком по одной. Это потребует изменения.protoи реализации интерфейса сSend. - Подключите интерцепторы (middleware) — для логирования, метрик, авторизации. В gRPC-Go они называются
UnaryInterceptorиStreamInterceptor. - Замените in-memory хранилище на базу данных и проверьте, как ведут себя таймауты контекста при медленных запросах.
Весь код из статьи написан для Go 1.22 и google.golang.org/grpc v1.65+. Если вы используете более раннюю версию библиотеки, API подключения клиента может отличаться — сверяйтесь с официальной документацией пакета.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: gRPC-Go — официальная документация и Quick Start, Protocol Buffers Language Guide (proto3)
Частые вопросы
Обязательно ли использовать protoc для генерации кода?
Можно ли вызвать gRPC-сервис из браузера напрямую?
Что будет, если я добавлю новое поле в .proto, но не обновлю клиент?
У меня другая версия Go или grpc — код не компилируется. Что делать?
Насколько gRPC быстрее REST на практике?
Что такое UnimplementedTaskServiceServer и зачем его встраивать?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.