Кодогенерация Protobuf файлов используя плагин Buf

В этой статье научимся генерировать Protobuf файлы через плагин Buf. Настроим зависимости сторонних proto нотаций, такие как googleapis, grpc-gateway и protovalidate.

Оглавление статьи

  1. Вводная часть
  2. Подготовительные работы
  3. Установка зависимостей
  4. Структура api
  5. Генерируем код
  6. Запускаем gRPC и REST сервер
  7. Подведем итоги

Вводная часть

Данная статья является дополнением предыдущей статьи “Создание сервера Golang с gRPC и Rest API используя Swagger”.

В прошлой статье зависимости проекта такие как google/api/annotations.proto и т.п. были скачены вручную, здесь же мы разберем как можно улучшить и автоматизировать данный процесс.

Подготовительные работы

Создадим отправную точку для нашего проекта:

BASH
# Создание модуля проекта
go mod init github.com/eliofery/golang-buf

# Создание файла команд проекта
touch Makefile

Файл Makefile

Содержимое файла Makefile:

MAKEFILE
# Путь до бинарных файлов
LOCAL_BIN=$(CURDIR)/bin

# Установка бинарных файлов различных пакетов.
# Плагины пакетов можно установить глобально выполнив команду go install [package].
# Но лучше сохранить их в каталоге проекта, зафиксировав используемые версии,
# чтобы не нарушить обратную совместимость при коллективной работе над проектом.
install-bin:
  GOBIN=$(LOCAL_BIN) go install github.com/bufbuild/buf/cmd/buf@v1.29.0
  GOBIN=$(LOCAL_BIN) go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
  GOBIN=$(LOCAL_BIN) go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
  GOBIN=$(LOCAL_BIN) go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2.19.1
  GOBIN=$(LOCAL_BIN) go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@v2.19.1

# Приводим проект в первоначальный вид
clean:
  rm -rf $(LOCAL_BIN)

Выполним команду make install-bin для скачивания бинарных файлов. В корне проекта создастся каталог bin со скаченными бинарниками. Теперь, чтобы вызвать тот или иной плагин в терминале необходимо будет ввести команду ./bin/[имя файла] [опции].

Установка зависимостей

Для кодогенерации proto файлов необходимо наличие Protobuf генератора. Подробная инструкция по его установке для различных ОС доступна на официальном сайте gRPC документации. Я буду производить установку в Linux среде дистрибутива PopOS!.

BASH
// Вариант 1
// Стандартный вариант установки
// Плюсы: ни чего дополнительно устанавливать не надо
// Минусы: устанавливается устаревшая версия
sudo apt update
sudo apt install -y protobuf-compiler

// Вариант 2
// Вариант установки через Snap пакет
// Плюсы: актуальная версия
// Минусы: нужно установить snap
sudo apt update
sudo apt install snapd
sudo snap install protobuf --classic

// Вариант 3
// Вариант со скачиванием бинарного файла
// Скачиваете последнюю версию protobuf с официального репозитория
// https://github.com/protocolbuffers/protobuf/releases
// И добавьте путь до бинарного файла protobuf в переменные среды,
// либо разместите бинарный файл в каталоге /usr/local/bin
// Плюсы: актуальная версия
// Минусы: сложность установки

// Для проверки работоспособности вводим команду
protoc --version

В предыдущем разделе мы скачали бинарники необходимых нам пакетов. Теперь необходимо так же скачать сами пакеты. При установке пакетов задаем конкретные версии для сохранения совместимости.

BASH
go get google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
go get google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
go get github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2.19.1
go get github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@v2.19.1
go get github.com/bufbuild/protovalidate-go@v0.6.0

Структура api

Официальная документация Buf предоставляет подробное описание того как должна выглядеть структура proto файлов и описание buf конфигураций. Советую ознакомиться:

Следуя выше изложенной документации создадим в корне проекта файлы buf.gen.yaml и buf.work.yaml.

buf.gen.yaml - конфигурационный файл содержит правила для генерации кода из proto файлов.

BUF.GEN.YAML
version: v1

# Настройки используемых плагинов.
# Здесь мы перечисляем скаченные раннее бинарные файлы плагинов,
# указываем путь где размещать сгенерированные .go файлы
# и прописываем различные дополнительные опции.
plugins:
  # Генерирует файлы .pb.go
  - name: go
    path: ./bin/protoc-gen-go
    out: ./pkg
    opt:
    - paths=source_relative

  # Генерирует файлы _grpc.pb.go
  - name: go-grpc
    path: ./bin/protoc-gen-go-grpc
    out: ./pkg
    opt:
    - paths=source_relative

  # Генерирует файлы .pb.gw.go
  - name: grpc-gateway
    path: ./bin/protoc-gen-grpc-gateway
    out: ./pkg
    opt:
    - paths=source_relative

  # Генерирует файлы .swagger.json
  - name: openapiv2
    path: ./bin/protoc-gen-openapiv2
    out: ./pkg

  # Генерирует один общий файл .swagger.json
  - name: openapiv2
    path: ./bin/protoc-gen-openapiv2
    out: ./docs
    opt:
    - allow_merge=true

buf.work.yaml - конфигурационный файл содержит перечень директорий в которых хранятся proto файлы.

BUF.GEN.YAML
version: v1

# Перечисление директорий, хранящих .proto файлы
directories:
  - api

В корне проекта создадим каталог api в котором будут располагаться все пользовательские proto файлы. Внутри каталога api создадим файл buf.yaml.

buf.yaml - конфигурационный файл содержит различные настройки такие как линтер, зависимости, исключаемые из генерации proto файлы и много другое.

BUF.YAML
version: v1

# Зависимости проекта
deps:
  - buf.build/googleapis/googleapis
  - buf.build/grpc-ecosystem/grpc-gateway
  - buf.build/bufbuild/protovalidate

# Базовые настройки линтера для proto файлов
lint:
  use:
    - DEFAULT

# Не вникал в этот параметр, но оставил его, так как он автоматически прописывается
# при создании файла buf.yaml через команду ./bin/buf mod init.
breaking:
  use:
    - FILE

После описания buf.yaml файла необходимо установить прописанные в нем зависимости, выполнив команду:

BASH
# api - название директории, где хранится файл buf.yaml.
# Если команда buf mod update вызывается из каталога, где находится файл buf.yaml, то имя директории можно не писать.
./bin/buf mod update api

После выполнения команды в Buf cache будут установлены прописанные зависимости и мы сможем воспользоваться их proto файлами. Далее мы на практике это рассмотрим. Так же рядом с файлом buf.yaml появится файл buf.lock который хранит именно те версии зависимостей, что мы установили. Это нужно опять же для обратной совместимости при командной работе.

И так, конфигурационные buf файлы настроены настала пора создать структуру наших proto файлов. Для этого в каталоге api создадим каталог, хранящий proto файлы. Я назову его microservice.

При развитии проекта бывает необходимость в кардинальном изменении API. В этом случае поможет версионирование proto файлов. Для этого внутри каталога microservice создадим каталог v1. В нем будут храниться proto файлы относящиеся к API первой версии. В каталоге v1 создадим два файла microservice_grpc.proto и user.proto.

  • microservice_grpc.proto - будет описывать service, хранящий rpc ручки.
  • user.proto - будет описывать message, хранищий сущности запроса, ответа нашего API для пользователей.

Каждая сущность описывается в своем proto файле, например: product.proto, payment.proto, upload.proto и т.п.
А файл microservice_grpc.proto их агрегирует.

Более подробное объяснение описания proto файлов было в прошлой статье, здесь я лишь коснусь кратко некоторых моментов.

Файл microservice_grpc.proto

Опишем содержимое файла microservice_grpc.proto:

MICROSERVICE_GRPC.PROTO
syntax = "proto3";

package microservice.v1;

option go_package = "github.com/eliofery/golang-fullstack/pkg/microservice/v1";

import "google/api/annotations.proto";
import "protoc-gen-openapiv2/options/annotations.proto";

import "microservice/v1/user.proto";

option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
  info: {
    title: "Echo API";
    version: "1.0";
    description: "";
    contact: {
      name: "gRPC-Gateway project";
      url: "https://github.com/grpc-ecosystem/grpc-gateway";
      email: "none@example.com";
    };
    license: {
      name: "BSD 3-Clause License";
      url: "https://github.com/grpc-ecosystem/grpc-gateway/blob/main/LICENSE";
    };
  };
  schemes: HTTPS;
  consumes: "application/json";
  produces: "application/json";
};

service MicroService {
  rpc User(UserRequest) returns (UserResponse) {
    option (google.api.http) = {
        post: "/v1/user"
        body: "*"
    };

    option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
        summary: "Get a message.";
        operation_id: "getMessage";
        tags: "echo";
        responses: {
            key: "201"
            value: {
                description: "OK Success";
            }
        }
    };
  }
}

В данном файле мы импортируем файлы google/api/annotations.proto и protoc-gen-openapiv2/options/annotations.proto. Мы можем воспользоваться ими благодаря тому, что установили зависимости, прописанные в файле buf.yaml, используя команду ./bin/buf mod update api.

Файл user.proto

Опишем содержимое файла user.proto:

USER.PROTO
syntax = "proto3";

package microservice.v1;

option go_package = "github.com/eliofery/golang-fullstack/pkg/microservice/v1";

import "protoc-gen-openapiv2/options/annotations.proto";
import "buf/validate/validate.proto";
import "google/protobuf/wrappers.proto";

message UserRequest {
  option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {
    json_schema: {
      title: "A bit of everything"
      description: "Intentionaly complicated message type to cover many features of Protobuf."
      required: ["number", "email", "password"]
    }
    external_docs: {
      url: "https://github.com/grpc-ecosystem/grpc-gateway";
      description: "Find out more about ABitOfEverything";
    }
    example: "{\"number\": \"538\",\"email\": \"test@mail.org\",\"password\": \"123456\"}"
  };

  int32 number = 1;
  string email = 2 [
      (buf.validate.field).string.email = true
  ];
}

message UserResponse {
  google.protobuf.StringValue result = 1;
}

В данном файле мы импортируем файлы protoc-gen-openapiv2/options/annotations.proto и buf/validate/validate.proto. Возможность использовать эти proto файлы так же доступно благодаря установки зависимостей прописанных в файле buf.yaml.

Версия 2

Скопируем каталог v1 со всеми файлами в ту же директорию и переименуем его в v2.

Содержимое файлов microservice_grpc.proto и user.proto будет следующим:

microservice_grpc.proto

MICROSERVICE_GRPC.PROTO
syntax = "proto3";

package microservice.v2;

option go_package = "github.com/eliofery/golang-fullstack/pkg/microservice/v2";

import "google/api/annotations.proto";

import "microservice/v2/user.proto";

service MicroService {
  rpc User(UserRequest) returns (UserResponse) {
    option (google.api.http) = {
      post: "/v2/user"
      body: "*"
    };
  }
}

user.proto

USER.PROTO
syntax = "proto3";

package microservice.v2;

option go_package = "github.com/eliofery/golang-fullstack/pkg/microservice/v2";

import "buf/validate/validate.proto";
import "google/protobuf/wrappers.proto";

message UserRequest {
    int32 number = 1;
    string email = 2 [
        (buf.validate.field).string.email = true
    ];
}

message UserResponse {
    google.protobuf.StringValue result = 1;
}

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

Перед тем как сгенерировать код из proto файлов давайте проверим соответствие наших proto файлов правилам линтера. Для это воспользуемся командой:

BASH
buf lint

При возникновении ошибок линтер подскажет, что не так. В нашем случае ошибок быть не должно и мы можем приступить к генерации кода:

BASH
buf generate

После выполнения команды в корне проекта появятся два каталога docs, хранящий Swagger файл с нашим API и pkg сгенерированные Protobuf файлы.

Ваш IDE может подсвечивать красным цветом сторонние зависимости proto файлов. Если вы пользуетесь IDE Goland то установите плагин Buf for Protocol Buffers для других редакторов ознакомьтесь с этим обсуждением возможно оно вам поможет.

Запускаем gRPC и REST сервер

В корне проекта создадим каталог cmd/grpc_server и внутри него файл main.go со следующим содержимым:

MAIN.GO
package main

import (
  "context"
  "fmt"
  pb "github.com/eliofery/golang-fullstack/pkg/microservice/v1"
  pbV2 "github.com/eliofery/golang-fullstack/pkg/microservice/v2"
  "github.com/golang/protobuf/ptypes/wrappers"
  "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
  "google.golang.org/grpc"
  "google.golang.org/grpc/reflection"
  "net"
  "net/http"
)

type MicroserviceServer struct {
  pb.UnimplementedMicroServiceServer
}

func (s *MicroserviceServer) User(ctx context.Context, req *pb.UserRequest) (*pb.UserResponse, error) {
  return &pb.UserResponse{
    Result: &wrappers.StringValue{Value: "success"},
  }, nil
}

type MicroserviceV2Server struct {
  pbV2.UnimplementedMicroServiceServer
}

func (s *MicroserviceV2Server) User(ctx context.Context, req *pbV2.UserRequest) (*pbV2.UserResponse, error) {
  return &pbV2.UserResponse{
    Result: &wrappers.StringValue{Value: "success v2"},
  }, nil
}

func main() {
  ch := make(chan error, 2)
  
  // gRPC server
  go func(ch chan error) {
    fmt.Println("gRPC server start :50051")
    listen, err := net.Listen("tcp", ":50051")
    if err != nil {
      ch <- err
    }
  
    grpcServer := grpc.NewServer()
    reflection.Register(grpcServer)
    
    pb.RegisterMicroServiceServer(grpcServer, &MicroserviceServer{})
    pbV2.RegisterMicroServiceServer(grpcServer, &MicroserviceV2Server{})
    
    if err = grpcServer.Serve(listen); err != nil {
      ch <- err
    }
  }(ch)
  
  // REST server
  go func() {
    mux := runtime.NewServeMux()

    err := pb.RegisterMicroServiceHandlerServer(context.Background(), mux, &MicroserviceServer{})
    if err != nil {
      ch <- err
    }

    err = pbV2.RegisterMicroServiceHandlerServer(context.Background(), mux, &MicroserviceV2Server{})
    if err != nil {
      ch <- err
    }
  
    server := http.Server{
      Addr:    ":8080",
      Handler: mux,
    }
    
    fmt.Println("REST server start :8080")
    if err = server.ListenAndServe(); err != nil {
      ch <- err
    }
  }()
  
  for i := 0; i < 2; i++ {
    if err := <-ch; err != nil {
      panic(err)
    }
  }
}

Мы уже разбирали этот код в прошлой статье, здесь я лишь добавил использование API версии первой и второй.

Для этого нужно зарегистрировать каждую версию нашего API после чего оно станет доступно по заданному адресу и порту:

MAIN.GO
// pb - сокращенно от protobuf
import (
  pb "github.com/eliofery/golang-fullstack/pkg/microservice/v1"
  pbV2 "github.com/eliofery/golang-fullstack/pkg/microservice/v2"
)

type MicroserviceServer struct {
  pb.UnimplementedMicroServiceServer
}

type MicroserviceV2Server struct {
  pbV2.UnimplementedMicroServiceServer
}

pb.RegisterMicroServiceServer(grpcServer, &MicroserviceServer{})
pbV2.RegisterMicroServiceServer(grpcServer, &MicroserviceV2Server{})

pb.RegisterMicroServiceHandlerServer(context.Background(), mux, &MicroserviceServer{})
pbV2.RegisterMicroServiceHandlerServer(context.Background(), mux, &MicroserviceV2Server{})

Подведем итоги

В этой статье мы расширили свои знания в области генерации proto кода используя плагин Buf. Данный вариант считается передовым и рекомендуемым по сравнению с предыдущим, который мы разбирали в прошлой статье. Хотя он тоже довольно не плох. Какой из этих подходов выбирать решайте сами.

Ссылки на проект

Предыдущая статья Создание сервера Golang с gRPC и Rest API используя Swagger