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

Как внедрить gRPC в Go-проект: создание proto-файлов, генерация кода, настройка сервера и клиента. Методы шаринга proto между микросервисами: git submodules, отдельный репозиторий, buf registry. Практические примеры из опыта.
- теги
- #Go #Программирование #Архитектура
- категории
- Programming
- опубликовано
Работаешь с микросервисной архитектурой на 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 соединений между микросервисами.