@grpc/grpc-js + protobuf-ts → google.golang.org/grpc + protoc-gen-go
.proto файлы → точно такие же! Protocol Buffers — язык-независимый IDL
new MyServiceClient(address) → pb.NewMyServiceClient(conn)
server.addService(MyService, handlers) → pb.RegisterMyServiceServer(s, &server{})
call.request → req.GetField() (геттеры)
.proto, типы, сервисыprotoc + protoc-gen-go + protoc-gen-go-grpcServe()mkdir go-grpc-basics && cd go-grpc-basics
go mod init go-grpc-basics
# Устанавливаем gRPC и protobuf
go get google.golang.org/grpc
go get google.golang.org/protobuf
go get github.com/google/uuid
go mod tidy
# Устанавливаем protoc (компилятор .proto) — macOS
brew install protobuf
# Или Linux
apt-get install -y protobuf-compiler
# Устанавливаем Go-плагины для protoc
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# Убедитесь, что $GOPATH/bin в PATH
export PATH="$PATH:$(go env GOPATH)/bin"
# Создаём директории
mkdir -p proto
mkdir -p server
mkdir -p client
go-grpc-basics/ ├── proto/ │ └── tasks/ │ └── tasks.proto # Определение сервиса и сообщений ├── server/ │ └── main.go # gRPC сервер ├── client/ │ └── main.go # gRPC клиент ├── go.mod └── go.sum
proto/tasks/tasks.proto// Версия синтаксиса protobuf (proto3 — современный стандарт)
syntax = "proto3";
// Пакет (для генерации кода)
package tasks;
// go_package определяет путь импорта в Go
// формат: полный_путь_модуля/относительный_путь;имя_пакета
option go_package = "go-grpc-basics/gen/tasks;taskspb";
// Импорт для google.protobuf.Timestamp
import "google/protobuf/timestamp.proto";
// ╔══════════════════════════════════════════════════════════╗
// ║ СЕРВИС (RPC методы) ║
// ╚══════════════════════════════════════════════════════════╝
// TaskService — сервис для управления задачами
service TaskService {
// Unary RPC: простой запрос-ответ
// rpc ИмяМетода(Запрос) returns (Ответ);
rpc CreateTask(CreateTaskRequest) returns (CreateTaskResponse);
rpc GetTask(GetTaskRequest) returns (GetTaskResponse);
rpc ListTasks(ListTasksRequest) returns (ListTasksResponse);
rpc UpdateTask(UpdateTaskRequest) returns (UpdateTaskResponse);
rpc DeleteTask(DeleteTaskRequest) returns (DeleteTaskResponse);
}
// ╔══════════════════════════════════════════════════════════╗
// ║ СООБЩЕНИЯ (Messages) ║
// ╚══════════════════════════════════════════════════════════╝
// Task — основная сущность
message Task {
string id = 1; // Номер поля важен для бинарной сериализации
string title = 2;
string description = 3;
TaskStatus status = 4; // enum
Priority priority = 5; // вложенный enum
google.protobuf.Timestamp created_at = 6;
google.protobuf.Timestamp updated_at = 7;
optional google.protobuf.Timestamp completed_at = 8; // optional = может быть null
repeated string tags = 9; // repeated = массив
string assignee = 10;
}
// TaskStatus — перечисление (enum)
enum TaskStatus {
TASK_STATUS_UNSPECIFIED = 0; // Всегда начинайте enum с 0 (значение по умолчанию)
TASK_STATUS_PENDING = 1;
TASK_STATUS_IN_PROGRESS = 2;
TASK_STATUS_DONE = 3;
TASK_STATUS_CANCELLED = 4;
}
// Priority — приоритет
enum Priority {
PRIORITY_UNSPECIFIED = 0;
PRIORITY_LOW = 1;
PRIORITY_MEDIUM = 2;
PRIORITY_HIGH = 3;
PRIORITY_CRITICAL = 4;
}
// ==========================================
// Запросы и ответы для каждого RPC
// ==========================================
message CreateTaskRequest {
string title = 1;
string description = 2;
Priority priority = 3;
repeated string tags = 4;
string assignee = 5;
}
message CreateTaskResponse {
Task task = 1;
}
message GetTaskRequest {
string id = 1;
}
message GetTaskResponse {
Task task = 1;
}
message ListTasksRequest {
// Фильтры (опциональные)
optional TaskStatus status = 1;
optional Priority priority = 2;
string assignee = 3;
// Пагинация
int32 page_size = 10; // Сколько элементов на странице
string page_token = 11; // Токен для следующей страницы
}
message ListTasksResponse {
repeated Task tasks = 1;
string next_page_token = 2; // Токен для следующей страницы (пустой = конец)
int32 total_count = 3; // Общее количество
}
message UpdateTaskRequest {
string id = 1;
// Поля, которые можно обновить (все опциональные)
optional string title = 2;
optional string description = 3;
optional TaskStatus status = 4;
optional Priority priority = 5;
repeated string tags = 6;
optional string assignee = 7;
}
message UpdateTaskResponse {
Task task = 1;
}
message DeleteTaskRequest {
string id = 1;
}
message DeleteTaskResponse {
bool success = 1;
}
# Создаём директорию для сгенерированного кода
mkdir -p gen/tasks
# Генерируем Go-код из .proto
protoc \
--proto_path=proto \
--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 — сгенерированные структуры (сообщения)
# gen/tasks/tasks_grpc.pb.go — сгенерированный сервер/клиент
server/main.go — gRPC Серверpackage main
import (
"context"
"fmt"
"log"
"net"
"os"
"os/signal"
"sync"
"syscall"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/google/uuid"
// Импорт сгенерированного кода
pb "go-grpc-basics/gen/tasks"
)
// ╔══════════════════════════════════════════════════════════╗
// ║ РЕАЛИЗАЦИЯ СЕРВЕРА ║
// ╚══════════════════════════════════════════════════════════╝
// TaskServer — реализует интерфейс TaskServiceServer (сгенерирован)
type TaskServer struct {
// UnimplementedTaskServiceServer ВСТРАИВАЕТСЯ для прямой совместимости.
// Если в будущем в .proto добавятся новые методы, старый код не сломается.
pb.UnimplementedTaskServiceServer
mu sync.RWMutex
tasks map[string]*pb.Task // In-memory хранилище
}
// NewTaskServer — конструктор
func NewTaskServer() *TaskServer {
return &TaskServer{
tasks: make(map[string]*pb.Task),
}
}
// CreateTask — реализация RPC метода
func (s *TaskServer) CreateTask(
ctx context.Context,
req *pb.CreateTaskRequest,
) (*pb.CreateTaskResponse, error) {
// Валидация
if req.Title == "" {
// status.Error создаёт gRPC-ошибку с кодом
return nil, status.Error(codes.InvalidArgument, "title must not be empty")
}
// Создаём задачу
now := timestamppb.Now() // Текущее время в protobuf-формате
task := &pb.Task{
Id: uuid.New().String(),
Title: req.Title,
Description: req.Description,
Status: pb.TaskStatus_TASK_STATUS_PENDING,
Priority: req.Priority,
CreatedAt: now,
UpdatedAt: now,
Tags: req.Tags,
Assignee: req.Assignee,
}
s.mu.Lock()
s.tasks[task.Id] = task
s.mu.Unlock()
log.Printf("📝 Задача создана: %s [%s]", task.Title, task.Id)
return &pb.CreateTaskResponse{Task: task}, nil
}
// GetTask — получение задачи по ID
func (s *TaskServer) GetTask(
ctx context.Context,
req *pb.GetTaskRequest,
) (*pb.GetTaskResponse, error) {
if req.Id == "" {
return nil, status.Error(codes.InvalidArgument, "id must not be empty")
}
s.mu.RLock()
task, ok := s.tasks[req.Id]
s.mu.RUnlock()
if !ok {
return nil, status.Errorf(codes.NotFound, "task %q not found", req.Id)
}
return &pb.GetTaskResponse{Task: task}, nil
}
// ListTasks — список задач с фильтрацией
func (s *TaskServer) ListTasks(
ctx context.Context,
req *pb.ListTasksRequest,
) (*pb.ListTasksResponse, error) {
s.mu.RLock()
defer s.mu.RUnlock()
var filtered []*pb.Task
for _, task := range s.tasks {
// Применяем фильтры
if req.Status != nil && task.Status != *req.Status {
continue
}
if req.Priority != nil && task.Priority != *req.Priority {
continue
}
if req.Assignee != "" && task.Assignee != req.Assignee {
continue
}
filtered = append(filtered, task)
}
// Пагинация
totalCount := int32(len(filtered))
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 10 // По умолчанию
}
// Простая offset-пагинация
start := 0
if req.PageToken != "" {
// В реальности — декодируем токен в offset
fmt.Sscanf(req.PageToken, "%d", &start)
}
end := start + int(pageSize)
if end > len(filtered) {
end = len(filtered)
}
var nextPageToken string
if end < len(filtered) {
nextPageToken = fmt.Sprintf("%d", end)
}
// Возвращаем пустой слайс, а не nil
result := filtered[start:end]
if result == nil {
result = []*pb.Task{}
}
return &pb.ListTasksResponse{
Tasks: result,
NextPageToken: nextPageToken,
TotalCount: totalCount,
}, nil
}
// UpdateTask — обновление задачи
func (s *TaskServer) UpdateTask(
ctx context.Context,
req *pb.UpdateTaskRequest,
) (*pb.UpdateTaskResponse, error) {
s.mu.Lock()
defer s.mu.Unlock()
task, ok := s.tasks[req.Id]
if !ok {
return nil, status.Errorf(codes.NotFound, "task %q not found", req.Id)
}
// Обновляем только переданные поля
if req.Title != nil {
task.Title = *req.Title
}
if req.Description != nil {
task.Description = *req.Description
}
if req.Status != nil {
task.Status = *req.Status
if *req.Status == pb.TaskStatus_TASK_STATUS_DONE {
task.CompletedAt = timestamppb.Now()
}
}
if req.Priority != nil {
task.Priority = *req.Priority
}
if req.Tags != nil {
task.Tags = req.Tags
}
if req.Assignee != nil {
task.Assignee = *req.Assignee
}
task.UpdatedAt = timestamppb.Now()
log.Printf("✏️ Задача обновлена: %s", task.Id)
return &pb.UpdateTaskResponse{Task: task}, nil
}
// DeleteTask — удаление задачи
func (s *TaskServer) DeleteTask(
ctx context.Context,
req *pb.DeleteTaskRequest,
) (*pb.DeleteTaskResponse, error) {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.tasks[req.Id]; !ok {
return nil, status.Errorf(codes.NotFound, "task %q not found", req.Id)
}
delete(s.tasks, req.Id)
log.Printf("🗑️ Задача удалена: %s", req.Id)
return &pb.DeleteTaskResponse{Success: true}, nil
}
// ╔══════════════════════════════════════════════════════════╗
// ║ MAIN (СЕРВЕР) ║
// ╚══════════════════════════════════════════════════════════╝
func main() {
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
log.Println("🚀 Запуск gRPC сервера...")
// Создаём TCP listener
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("Не удалось открыть порт: %v", err)
}
// Создаём gRPC сервер
grpcServer := grpc.NewServer(
// Можно добавить интерцепторы (логирование, трейсинг — урок 18)
)
// Регистрируем наш сервис
taskServer := NewTaskServer()
pb.RegisterTaskServiceServer(grpcServer, taskServer)
log.Println("✅ gRPC сервер слушает на :50051")
log.Println("Доступные методы:")
log.Println(" - CreateTask")
log.Println(" - GetTask")
log.Println(" - ListTasks")
log.Println(" - UpdateTask")
log.Println(" - DeleteTask")
// Graceful shutdown
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
log.Println("🛑 Выключение сервера...")
grpcServer.GracefulStop()
}()
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("Ошибка сервера: %v", err)
}
log.Println("✅ Сервер остановлен")
}
client/main.go — gRPC Клиентpackage main
import (
"context"
"fmt"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/status"
pb "go-grpc-basics/gen/tasks"
)
func main() {
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
log.Println("🔌 Подключение к gRPC серверу...")
// Устанавливаем соединение
// insecure — без TLS (для разработки!)
conn, err := grpc.Dial(
"localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(), // Ждать подключения
grpc.WithTimeout(5*time.Second),
)
if err != nil {
log.Fatalf("Ошибка подключения: %v", err)
}
defer conn.Close()
// Создаём клиент (stub)
client := pb.NewTaskServiceClient(conn)
ctx := context.Background()
// ==========================================
// 1. CREATE — создаём задачи
// ==========================================
log.Println("\n── CREATE ──")
tasks := []struct {
title string
priority pb.Priority
assignee string
}{
{"Изучить gRPC", pb.Priority_PRIORITY_HIGH, "alice"},
{"Написать микросервис", pb.Priority_PRIORITY_CRITICAL, "bob"},
{"Настроить CI/CD", pb.Priority_PRIORITY_MEDIUM, "alice"},
}
var createdIDs []string
for _, t := range tasks {
resp, err := client.CreateTask(ctx, &pb.CreateTaskRequest{
Title: t.title,
Priority: t.priority,
Assignee: t.assignee,
Tags: []string{"demo", "grpc"},
})
if err != nil {
log.Printf(" ❌ Ошибка: %v", err)
continue
}
createdIDs = append(createdIDs, resp.Task.Id)
log.Printf(" ✓ Создана: [%s] %s", resp.Task.Id[:8], resp.Task.Title)
}
if len(createdIDs) == 0 {
log.Fatal("Не создано ни одной задачи")
}
// ==========================================
// 2. GET — получаем задачу
// ==========================================
log.Println("\n── GET ──")
resp, err := client.GetTask(ctx, &pb.GetTaskRequest{Id: createdIDs[0]})
if err != nil {
// Извлекаем gRPC статус
st, _ := status.FromError(err)
log.Printf(" ❌ Ошибка: код=%s сообщение=%s", st.Code(), st.Message())
} else {
task := resp.Task
log.Printf(" ✓ Задача: %s", task.Title)
log.Printf(" Статус: %s", task.Status)
log.Printf(" Приоритет: %s", task.Priority)
log.Printf(" Создана: %s", task.CreatedAt.AsTime().Format(time.RFC3339))
}
// ==========================================
// 3. LIST — список задач
// ==========================================
log.Println("\n── LIST ──")
listResp, err := client.ListTasks(ctx, &pb.ListTasksRequest{
Assignee: "alice",
PageSize: 10,
})
if err != nil {
log.Printf(" ❌ Ошибка: %v", err)
} else {
log.Printf(" ✓ Задачи Alice (всего %d):", listResp.TotalCount)
for _, task := range listResp.Tasks {
log.Printf(" - [%s] %s (%s)", task.Id[:8], task.Title, task.Status)
}
}
// ==========================================
// 4. UPDATE — обновляем задачу
// ==========================================
log.Println("\n── UPDATE ──")
doneStatus := pb.TaskStatus_TASK_STATUS_DONE
updateResp, err := client.UpdateTask(ctx, &pb.UpdateTaskRequest{
Id: createdIDs[0],
Status: &doneStatus,
})
if err != nil {
log.Printf(" ❌ Ошибка: %v", err)
} else {
task := updateResp.Task
log.Printf(" ✓ Обновлена: [%s] %s → %s", task.Id[:8], task.Title, task.Status)
if task.CompletedAt != nil {
log.Printf(" Завершена: %s", task.CompletedAt.AsTime().Format(time.RFC3339))
}
}
// ==========================================
// 5. DELETE — удаляем задачу
// ==========================================
log.Println("\n── DELETE ──")
deleteResp, err := client.DeleteTask(ctx, &pb.DeleteTaskRequest{
Id: createdIDs[len(createdIDs)-1],
})
if err != nil {
log.Printf(" ❌ Ошибка: %v", err)
} else {
log.Printf(" ✓ Удалена: success=%v", deleteResp.Success)
}
// ==========================================
// 6. ОШИБКИ — демонстрация
// ==========================================
log.Println("\n── ОШИБКИ ──")
// Попытка получить несуществующую задачу
_, err = client.GetTask(ctx, &pb.GetTaskRequest{Id: "nonexistent"})
if err != nil {
st, _ := status.FromError(err)
log.Printf(" Ожидаемая ошибка: код=%s, сообщение=%q", st.Code(), st.Message())
}
// Попытка создать задачу без заголовка
_, err = client.CreateTask(ctx, &pb.CreateTaskRequest{
Title: "", // Пустой заголовок
})
if err != nil {
st, _ := status.FromError(err)
log.Printf(" Валидация: код=%s, сообщение=%q", st.Code(), st.Message())
}
log.Println("\n✅ Клиент завершил работу")
}
# Терминал 1: Запускаем сервер
go run ./server/main.go
# Терминал 2: Запускаем клиент
go run ./client/main.go
| Характеристика | REST (JSON/HTTP) | gRPC (Protobuf/HTTP2) |
|---|---|---|
| Формат | JSON (текстовый) | Protobuf (бинарный, в 5-10x меньше) |
| Транспорт | HTTP 1.1 | HTTP/2 (мультиплексирование) |
| Контракт | OpenAPI (опционально) | .proto (обязательно) |
| Генерация кода | Опционально | Встроена (protoc) |
| Стриминг | WebSocket (отдельно) | Встроен (unary, server, client, bidirectional) |
| Обратная совместимость | Сложнее | Встроена (новые поля игнорируются) |
💡 Best practices от сеньоров:
req.GetTitle() вместо req.Title (безопасны для nil).💡 Для Node.js разработчика:
@grpc/grpc-js + protobuf-ts. В Go — стандартный google.golang.org/grpc.
.proto файлы идентичны. Разница только в генерации кода.new MyServiceClient(address, credentials). В Go: pb.NewMyServiceClient(conn).{ code: 5, message: "..." }. В Go: status.Error(codes.NotFound, "...").undefined.