gRPC в Go: внедрение и шаринг proto между микросервисами

gRPC в Go: внедрение и шаринг proto между микросервисами

Как внедрить gRPC в Go-проект: создание proto-файлов, генерация кода, настройка сервера и клиента. Методы шаринга proto между микросервисами: git submodules, отдельный репозиторий, buf registry. Практические примеры из опыта.

Работаешь с микросервисной архитектурой на Go или Golang? Рано или поздно надоедает гонять JSON туда-сюда. REST API хорош для внешних клиентов, но для внутренней коммуникации между сервисами есть вариант получше — gRPC.

Недавно пришлось внедрять gRPC в Go проект, и столкнулся с вопросом: как правильно организовать работу с proto-файлами (Protocol Buffers), когда у тебя несколько репозиториев? Поделюсь опытом, что попробовал и что сработало. Если интересно, как не писать говнокод на Go, почитай мою статью про антипаттерны в Go коде — там реальные примеры из production.

Зачем вообще это нужно?

Честно говоря, поначалу думал “нахуй оно мне надо, REST и так работает”. Но когда микросервисов стало больше, начались проблемы с производительностью:

  • JSON парсится медленно, особенно на больших объемах
  • Нет типизации — можно случайно отправить string вместо int, и никто не заметит до рантайма
  • HTTP/1.1 — куча оверхеда на каждый запрос
  • Нет streaming из коробки

gRPC для Go решает эти проблемы:

  • Типизированные контракты — Protocol Buffers жестко описывают структуру данных, никаких сюрпризов
  • HTTP/2 — мультиплексирование, меньше оверхеда
  • Бинарная сериализация — быстрее и компактнее JSON
  • Streaming — встроенная поддержка потоковой передачи
  • Кодогенерация — автоматическая генерация клиента и сервера на Go
  • Производительность — в 5-10 раз быстрее REST для внутренней коммуникации между микросервисами

Если у тебя микросервисная архитектура и сервисы общаются между собой, gRPC даст ощутимый прирост производительности. Проверено на практике. Кстати, если интересна тема архитектуры и как не делать циклические зависимости — почитай про антипаттерны в Go коде , там есть реальные примеры проблем из production.

Что нужно установить для gRPC в Go

Для работы с gRPC в Go или Golang нужно поставить несколько инструментов:

# Установка protoc (Protocol Buffers compiler)
# Для macOS
brew install protobuf

# Для Linux
apt 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

# Добавь в PATH, если еще не добавил
export PATH="$PATH:$(go env GOPATH)/bin"

Проверяем, что все на месте:

protoc --version
# libprotoc 3.21.12 (или выше)

Если видишь версию — все ок, можно продолжать.

Создаем первый proto-файл (Protocol Buffers)

Для примера сделаем простой gRPC сервис на Go для управления пользователями. Структура проекта такая:

myproject/
├── api/
│   └── proto/
│       └── user/
│           └── v1/
│               └── user.proto
├── cmd/
│   ├── server/
│   │   └── main.go
│   └── client/
│       └── main.go
├── internal/
│   └── service/
│       └── user.go
├── go.mod
└── Makefile

Создаем api/proto/user/v1/user.proto:

syntax = "proto3";

package user.v1;

option go_package = "github.com/yourusername/myproject/gen/go/user/v1;userv1";

import "google/protobuf/timestamp.proto";

// User service для управления пользователями
service UserService {
  // Создание нового пользователя
  rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
  
  // Получение пользователя по ID
  rpc GetUser(GetUserRequest) returns (GetUserResponse);
  
  // Получение списка пользователей (с поддержкой stream)
  rpc ListUsers(ListUsersRequest) returns (stream User);
  
  // Обновление пользователя
  rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse);
  
  // Удаление пользователя
  rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse);
}

// Модель пользователя
message User {
  string id = 1;
  string email = 2;
  string name = 3;
  google.protobuf.Timestamp created_at = 4;
  google.protobuf.Timestamp updated_at = 5;
}

message CreateUserRequest {
  string email = 1;
  string name = 2;
}

message CreateUserResponse {
  User user = 1;
}

message GetUserRequest {
  string id = 1;
}

message GetUserResponse {
  User user = 1;
}

message ListUsersRequest {
  int32 page = 1;
  int32 page_size = 2;
}

message UpdateUserRequest {
  string id = 1;
  string email = 2;
  string name = 3;
}

message UpdateUserResponse {
  User user = 1;
}

message DeleteUserRequest {
  string id = 1;
}

message DeleteUserResponse {
  bool success = 1;
}

Генерируем Go код из proto-файлов

Чтобы не вводить длинную команду каждый раз для генерации Go кода из Protocol Buffers, сделаем Makefile:

.PHONY: proto
proto:
	@echo "Generating proto files..."
	@mkdir -p gen/go
	@protoc \
		--proto_path=api/proto \
		--go_out=gen/go \
		--go_opt=paths=source_relative \
		--go-grpc_out=gen/go \
		--go-grpc_opt=paths=source_relative \
		api/proto/user/v1/*.proto
	@echo "Proto generation completed!"

.PHONY: clean
clean:
	@rm -rf gen/

Генерируем код:

make proto

Получишь два файла:

  • gen/go/user/v1/user.pb.go — структуры данных
  • gen/go/user/v1/user_grpc.pb.go — интерфейсы сервера и клиента

Делаем gRPC сервер на Go

Создаем internal/service/user.go для нашего gRPC сервиса:

package service

import (
	"context"
	"fmt"
	"time"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/timestamppb"

	userv1 "github.com/yourusername/myproject/gen/go/user/v1"
)

type UserService struct {
	userv1.UnimplementedUserServiceServer
	users map[string]*userv1.User
}

func NewUserService() *UserService {
	return &UserService{
		users: make(map[string]*userv1.User),
	}
}

func (s *UserService) CreateUser(ctx context.Context, req *userv1.CreateUserRequest) (*userv1.CreateUserResponse, error) {
	// Валидация
	if req.Email == "" {
		return nil, status.Error(codes.InvalidArgument, "email is required")
	}

	// Генерация ID (в production используйте UUID)
	id := fmt.Sprintf("user_%d", time.Now().UnixNano())
	now := timestamppb.Now()

	user := &userv1.User{
		Id:        id,
		Email:     req.Email,
		Name:      req.Name,
		CreatedAt: now,
		UpdatedAt: now,
	}

	s.users[id] = user

	return &userv1.CreateUserResponse{
		User: user,
	}, nil
}

func (s *UserService) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) {
	user, ok := s.users[req.Id]
	if !ok {
		return nil, status.Error(codes.NotFound, "user not found")
	}

	return &userv1.GetUserResponse{
		User: user,
	}, nil
}

func (s *UserService) ListUsers(req *userv1.ListUsersRequest, stream userv1.UserService_ListUsersServer) error {
	// Streaming response - отправляем пользователей по одному
	for _, user := range s.users {
		if err := stream.Send(user); err != nil {
			return err
		}
	}
	return nil
}

func (s *UserService) UpdateUser(ctx context.Context, req *userv1.UpdateUserRequest) (*userv1.UpdateUserResponse, error) {
	user, ok := s.users[req.Id]
	if !ok {
		return nil, status.Error(codes.NotFound, "user not found")
	}

	// Обновляем только переданные поля
	if req.Email != "" {
		user.Email = req.Email
	}
	if req.Name != "" {
		user.Name = req.Name
	}
	user.UpdatedAt = timestamppb.Now()

	return &userv1.UpdateUserResponse{
		User: user,
	}, nil
}

func (s *UserService) DeleteUser(ctx context.Context, req *userv1.DeleteUserRequest) (*userv1.DeleteUserResponse, error) {
	_, ok := s.users[req.Id]
	if !ok {
		return nil, status.Error(codes.NotFound, "user not found")
	}

	delete(s.users, req.Id)

	return &userv1.DeleteUserResponse{
		Success: true,
	}, nil
}

Создаем cmd/server/main.go:

package main

import (
	"fmt"
	"log"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"

	userv1 "github.com/yourusername/myproject/gen/go/user/v1"
	"github.com/yourusername/myproject/internal/service"
)

func main() {
	// Создаем TCP listener
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	// Создаем gRPC сервер
	grpcServer := grpc.NewServer()

	// Регистрируем наш сервис
	userService := service.NewUserService()
	userv1.RegisterUserServiceServer(grpcServer, userService)

	// Включаем reflection для grpcurl и Postman
	reflection.Register(grpcServer)

	fmt.Println("gRPC server listening on :50051")
	if err := grpcServer.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

Запускаем сервер:

go run cmd/server/main.go

Делаем gRPC клиент на Go

Создаем cmd/client/main.go для подключения к нашему gRPC серверу:

package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

	userv1 "github.com/yourusername/myproject/gen/go/user/v1"
)

func main() {
	// Подключаемся к серверу
	conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("failed to connect: %v", err)
	}
	defer conn.Close()

	// Создаем клиент
	client := userv1.NewUserServiceClient(conn)
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// Создаем пользователя
	createResp, err := client.CreateUser(ctx, &userv1.CreateUserRequest{
		Email: "test@example.com",
		Name:  "Test User",
	})
	if err != nil {
		log.Fatalf("CreateUser failed: %v", err)
	}
	fmt.Printf("Created user: %v\n", createResp.User)

	// Получаем пользователя
	getResp, err := client.GetUser(ctx, &userv1.GetUserRequest{
		Id: createResp.User.Id,
	})
	if err != nil {
		log.Fatalf("GetUser failed: %v", err)
	}
	fmt.Printf("Got user: %v\n", getResp.User)

	// Создаем еще несколько пользователей
	for i := 0; i < 3; i++ {
		_, err := client.CreateUser(ctx, &userv1.CreateUserRequest{
			Email: fmt.Sprintf("user%d@example.com", i),
			Name:  fmt.Sprintf("User %d", i),
		})
		if err != nil {
			log.Fatalf("CreateUser failed: %v", err)
		}
	}

	// Получаем список пользователей через stream
	stream, err := client.ListUsers(ctx, &userv1.ListUsersRequest{})
	if err != nil {
		log.Fatalf("ListUsers failed: %v", err)
	}

	fmt.Println("\nAll users:")
	for {
		user, err := stream.Recv()
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Fatalf("ListUsers stream error: %v", err)
		}
		fmt.Printf("  - %s: %s (%s)\n", user.Id, user.Name, user.Email)
	}
}

Запускаем клиент:

go run cmd/client/main.go

Как шарить proto между репозиториями в микросервисной архитектуре

Вот тут начинается самое интересное. Когда у тебя микросервисная архитектура и несколько сервисов на Go в разных репозиториях, возникает вопрос: как организовать работу с proto-файлами (Protocol Buffers)? Я пробовал несколько вариантов для Go проектов, расскажу что сработало, а что нет.

Вариант 1: Git Submodules

Самый простой способ — сделать отдельный репозиторий с proto и подключить его как submodule. Я так делал в начале, когда команда была маленькая.

# Создаем репозиторий с proto
mkdir myproject-proto
cd myproject-proto
git init

# Структура
# myproject-proto/
# ├── user/v1/user.proto
# ├── order/v1/order.proto
# └── payment/v1/payment.proto

# В каждом сервисе добавляем submodule
cd ../user-service
git submodule add https://github.com/company/myproject-proto.git api/proto
git submodule update --init --recursive

Плюсы:

  • Настроить легко, буквально за пару минут
  • Работает из коробки с Git
  • Версионирование есть

Минусы:

  • Нужно вручную обновлять submodules (git submodule update --remote)
  • Можно забыть закоммитить обновление submodule (я так делал постоянно)
  • Конфликты, когда несколько человек работают одновременно

Когда использовать: Маленькая команда (2-5 человек), простые проекты. Для больших команд не очень удобно.

Вариант 2: Отдельный репозиторий с Go модулем

Этот вариант я пробовал, когда команда выросла и микросервисов стало больше. Идея простая: делаем репозиторий с proto-файлами (Protocol Buffers), генерируем в нем Go код и публикуем как обычный Go модуль.

Структура репозитория myproject-proto:

myproject-proto/
├── proto/
│   ├── user/v1/user.proto
│   ├── order/v1/order.proto
│   └── payment/v1/payment.proto
├── gen/go/
│   ├── user/v1/
│   ├── order/v1/
│   └── payment/v1/
├── go.mod
├── Makefile
└── buf.gen.yaml

go.mod:

module github.com/company/myproject-proto

go 1.21

require (
	google.golang.org/grpc v1.60.0
	google.golang.org/protobuf v1.32.0
)

Makefile:

.PHONY: generate
generate:
	@protoc \
		--proto_path=proto \
		--go_out=gen/go \
		--go_opt=paths=source_relative \
		--go-grpc_out=gen/go \
		--go-grpc_opt=paths=source_relative \
		proto/**/**/*.proto
	@git add gen/
	@echo "Generated Go code committed"

В других сервисах просто импортируешь:

import (
    userv1 "github.com/company/myproject-proto/gen/go/user/v1"
)
# В сервисах
go get github.com/company/myproject-proto@v1.2.3

Плюсы:

  • Интеграция простая — обычный go get, как с любой библиотекой
  • Версионирование через Git tags работает из коробки
  • Сгенерированный код в репозитории (можно посмотреть, что получилось)

Минусы:

  • Сгенерированный код в Git (репозиторий раздувается, но не критично)
  • Нужно делать releases вручную (можно автоматизировать через CI/CD)
  • При изменении proto нужно коммитить и generated код (немного неудобно)

Когда использовать: Средняя команда (5-15 человек), когда нужна простота и нативная интеграция с Go. Я использовал этот вариант, пока не перешел на Buf.

Вариант 3: Buf Schema Registry

Этот вариант я использую сейчас для больших Go проектов с микросервисной архитектурой. Buf — современный инструмент для работы с Protocol Buffers. По сути, это registry для proto-файлов, как npm для JS пакетов, только для Protocol Buffers.

Установка:

brew install bufbuild/buf/buf
# или
go install github.com/bufbuild/buf/cmd/buf@latest

Создаем buf.yaml в репозитории с proto:

version: v1
name: buf.build/company/myproject
breaking:
  use:
    - FILE
lint:
  use:
    - DEFAULT

Создаем buf.gen.yaml для генерации:

version: v1
managed:
  enabled: true
  go_package_prefix:
    default: github.com/company/myproject-proto/gen/go
plugins:
  - plugin: buf.build/protocolbuffers/go
    out: gen/go
    opt: paths=source_relative
  - plugin: buf.build/grpc/go
    out: gen/go
    opt: paths=source_relative

Генерация кода:

buf generate

Публикация в Buf Schema Registry:

# Логин
buf registry login

# Push proto в registry
buf push

В других сервисах создаем buf.gen.yaml:

version: v1
managed:
  enabled: true
plugins:
  - plugin: buf.build/protocolbuffers/go
    out: gen/go
    opt: paths=source_relative
  - plugin: buf.build/grpc/go
    out: gen/go
    opt: paths=source_relative

И генерируешь из registry:

buf generate buf.build/company/myproject

Плюсы:

  • Централизованное хранилище proto (все в одном месте)
  • Breaking changes detection — автоматически проверяет совместимость (очень удобно)
  • Версионирование и dependency management работают из коробки
  • Не нужно коммитить generated код (чище репозиторий)
  • Линтинг и валидация из коробки (меньше ошибок)
  • Приватные registry для enterprise (можно развернуть свой)

Минусы:

  • Дополнительный инструмент в стеке (нужно изучить)
  • Зависимость от внешнего сервиса (хотя можно развернуть свой Buf Registry)
  • Нужно время на изучение (но оно того стоит)

Когда использовать: Большая команда (15+ человек), много микросервисов в Go проектах, когда важна совместимость и автоматизация. Я перешел на Buf для работы с Protocol Buffers, когда команда выросла, и не жалею. Кстати, для автоматизации рутинных задач в Go очень помогает Cursor AI — он отлично справляется с генерацией кода и рефакторингом.

Вариант 4: Автогенерация в CI/CD

Если у тебя уже настроен CI/CD, можно автоматизировать генерацию кода там. Я не пробовал, но видел, как это делают в больших компаниях.

.github/workflows/generate.yml:

name: Generate Proto

on:
  push:
    branches: [main]
    paths:
      - 'proto/**'

jobs:
  generate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Go
        uses: actions/setup-go@v4
        with:
          go-version: '1.21'
      
      - name: Install protoc
        run: |
          sudo apt-get install -y protobuf-compiler
          go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
          go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
      
      - name: Generate code
        run: make generate
      
      - name: Commit and push
        run: |
          git config user.name "GitHub Actions"
          git config user.email "actions@github.com"
          git add gen/
          git commit -m "Generate proto code" || exit 0
          git push
      
      - name: Create release
        run: |
          VERSION="v1.0.${{ github.run_number }}"
          git tag $VERSION
          git push origin $VERSION

Плюсы:

  • Полная автоматизация (ничего делать не нужно)
  • Нет человеческого фактора (не забудешь обновить)
  • Версионирование автоматическое

Минусы:

  • Сложнее настройка (нужно разобраться с CI/CD)
  • Нужен CI/CD (если его нет, не вариант)

Когда использовать: Enterprise проекты с налаженным CI/CD. Для маленьких команд это оверкилл.

Что выбрать для Go проекта?

Короче, вот что я понял, работая с gRPC в Go и микросервисной архитектурой:

ВариантСложность настройкиУдобствоАвтоматизацияДля команды
Git SubmodulesНизкаяСреднеНет2-5 чел
Отдельный репозиторийНизкаяХорошоЧастично5-15 чел
Buf RegistryСредняяОтличноДа15+ чел
CI/CD генерацияВысокаяОтличноДаEnterprise

Мой совет для Go разработчиков: начни с submodules или отдельного репозитория для proto-файлов, а когда команда вырастет и микросервисов станет больше — переходи на Buf. Не нужно сразу делать сложную инфраструктуру, если команда маленькая.

Что еще нужно для production в Go проекте

Для production gRPC сервера на Go стоит добавить несколько важных вещей:

Middleware для логирования

package middleware

import (
	"context"
	"log"
	"time"

	"google.golang.org/grpc"
)

func LoggingInterceptor(
	ctx context.Context,
	req interface{},
	info *grpc.UnaryServerInfo,
	handler grpc.UnaryHandler,
) (interface{}, error) {
	start := time.Now()

	resp, err := handler(ctx, req)

	log.Printf(
		"method=%s duration=%s error=%v",
		info.FullMethod,
		time.Since(start),
		err,
	)

	return resp, err
}

Graceful shutdown

package main

import (
	"context"
	"log"
	"net"
	"os"
	"os/signal"
	"syscall"

	"google.golang.org/grpc"
)

func main() {
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	grpcServer := grpc.NewServer(
		grpc.UnaryInterceptor(middleware.LoggingInterceptor),
	)

	// ... регистрация сервисов ...

	// Graceful shutdown
	go func() {
		if err := grpcServer.Serve(lis); err != nil {
			log.Fatalf("failed to serve: %v", err)
		}
	}()

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit

	log.Println("Shutting down gRPC server...")
	grpcServer.GracefulStop()
	log.Println("Server stopped")
}

Полезные инструменты для работы с gRPC в Go

Когда работаешь с gRPC в Go проектах, эти инструменты очень помогают:

  • grpcurl — curl для gRPC, тестируешь API из терминала
  • grpcui — веб-интерфейс для gRPC (типа Swagger, только для gRPC)
  • Evans — интерактивный gRPC клиент
  • Buf — линтинг, breaking changes detection, schema registry
  • Postman — поддержка gRPC из коробки (удобно для тестирования)

Установка grpcurl:

brew install grpcurl

# Пример использования
grpcurl -plaintext localhost:50051 list
grpcurl -plaintext -d '{"email":"test@example.com","name":"Test"}' \
  localhost:50051 user.v1.UserService/CreateUser

Я использую grpcurl для быстрого тестирования, очень удобно. Кстати, если нужна помощь с автоматизацией рутинных задач в Go — попробуй Cursor AI , он отлично помогает с рефакторингом и генерацией кода.

Итого

gRPC для Go (Golang) — штука полезная для микросервисной архитектуры. Дает типизацию через Protocol Buffers, производительность и удобство разработки. Проверено на практике в реальных Go проектах.

Что касается шаринга proto-файлов между микросервисами, мой совет такой:

  • Маленькая команда (2-5 чел) → Git Submodules (проще всего для Go проектов)
  • Средняя команда (5-15 чел) → Отдельный Go модуль (удобно, нативная интеграция)
  • Большая команда (15+ чел) → Buf Registry (лучший вариант для Protocol Buffers)
  • Enterprise → CI/CD с автогенерацией (если есть инфраструктура)

Начни с простого (submodules или отдельный репозиторий), а когда Go проект вырастет и микросервисов станет больше — переходи на Buf. Не нужно сразу делать сложную инфраструктуру.

Если интересно, как не писать говнокод на Go, почитай мою статью про антипаттерны в Go коде — там реальные примеры из production. А для автоматизации рутинных задач в Go очень рекомендую Cursor AI — он отлично помогает с генерацией кода и рефакторингом.


P.S. Не забывай версионировать proto-файлы (Protocol Buffers) в Go проектах (v1, v2), чтобы не сломать совместимость при изменениях. И да, в production используй TLS для gRPC соединений между микросервисами.