- ArangoDB, Golang, Grpc (святая троица микросервисов)
- Golang:
- Микросервисы.
- gRPC (google Remote Procedure Call)
- GrapQL
- Буфер протокола
- Генераторы кода
- ArangoDB
- Начнем ….
- установка prototool:
- Теперь давайте установим protoc…
- Архитектура проекта
- Структура файлов проекта
- Создание нашего API для велосипедов и проката
- В нашем коде выше
- Создание подключения к базе данных…..
ArangoDB, Golang, Grpc (святая троица микросервисов)
В этой серии из двух частей я хочу построить простое микросервисное приложение для аренды велосипедов, чтобы продемонстрировать интеграцию gRPC с graphql-клиентом с помощью go и сохранение данных в базе данных ArangoDb. Это будет разделено на следующие части:
-
В первой части этой серии мы разработаем два API на основе фреймворка gRPC, «Users API», который будет представлять людей, которые будут арендовать наши велосипеды. Второй API будет «Bikes API», затем мы заставим эти два сервиса поддерживать независимые экземпляры баз данных в базе данных ArangoDB.
-
Во второй части мы свяжем два вышеупомянутых сервиса с graphql-клиентом.
Код этого проекта можно найти «здесь».
Технологии, архитектуры и FrameWorks будут реализованы и использованы следующие:
Golang:
Официально известен как язык программирования Go. Go — это язык, который был создан с нуля для параллелизма. Он был разработан и создан инженерами Google. Это статически типизированный, нативно компилируемый, собираемый из мусора, параллельный, пост-OOP (объектно-ориентированный язык).
В современном ландшафте распределенных облачных вычислений, небольшая справка: распределенная система — это совокупность независимых компьютеров, которая представляется пользователям как единая целостная система. Сейчас Golang особенно сияет в этой сфере, используя параллелизм. Параллелизм в Go — это способность функций выполняться независимо друг от друга. Механизмы параллелизма позволяют легко писать программы, которые максимально используют возможности многоядерных и сетевых машин (распределенных систем). Для этого используются Goroutines — независимые единицы работы, которые планируются и затем выполняются на доступном логическом процессоре. Ознакомьтесь с их официальным сайтом.
Микросервисы.
Архитектура микросервисов позволяет разработчикам декомпозировать наши приложения на относительно небольшие приложения (сервисы), слабо связанные друг с другом. Эти небольшие приложения могут быть созданы для решения конкретных уникальных бизнес-задач независимо друг от друга с использованием различных программных и аппаратных стеков, а развертывание и масштабирование после запуска также может быть выполнено независимо.
Такая структура обеспечивает множество преимуществ, таких как гибкость в разработке, надежность в развертывании и эксплуатации и модульность в масштабировании. Погрузитесь глубже здесь.
gRPC (google Remote Procedure Call)
Это фреймворк RPC (Remote Procedure Call), разработанный инженерами google. Он позволяет нам внедрять интерфейсы прикладного программирования (API) на основе RPC на HTTP/2 в отличие от API на основе REST.
Благодаря использованию HTTP/2 gRPC является более эффективным, HTTP/2 может выполнять несколько запросов параллельно на долгоживущем соединении в потокобезопасном режиме. Полезная нагрузка в gRPC основана на двоичном формате, что делает ее меньше, чем сообщения на основе JSON. Кроме того, HTTP/2 имеет встроенную функцию сжатия заголовков.
Посетите их сайт
GrapQL
Graphql — это язык запросов API, который был разработан для устранения недостатков REST, таких как часто встречающаяся избыточная и недостаточная выборка. Он был специально разработан для обеспечения гибкости и эффективности. Он также позволяет быстро итерации продукта на нашем front-end. Подробнее об этом здесь.
Буфер протокола
Protobuf — это независимый от платформы и языка метод сериализации данных, который позволяет нам изначально описывать наши данные в виде сообщений. Затем он позволяет нам определить набор операций над «сообщениями», которые мы только что определили в формате запрос/ответ. Погрузиться глубже в эту тему можно здесь.
Генераторы кода
Мы будем использовать генераторы кода в нашем процессе конструирования для создания некоторых частей кода нашего приложения. Эта возможность позволит нам потратить время на создание бизнес-логики сервисов, а не на низкоуровневую логику реализации API, о которой мы должны позаботиться автоматически. Мы будем использовать Prototool в этой первой части и gqlgen во второй.
ArangoDB
Это база данных NOSQL, созданная для обеспечения высокой доступности и масштабируемости, идеально подходящая для реализации персистентности в микросервисах. ArangoDB — это нативная мультимодельная база данных с открытым исходным кодом, которая поддерживает графовые, документальные и ключевые модели данных, позволяя пользователям свободно комбинировать все модели данных в одном запросе. Более подробно об этой базе данных и ее возможностях читайте здесь.
Начнем ….
Прежде всего, давайте убедимся, что все наши генераторы кода установлены и работают,
установка prototool:
Prototool в первую очередь обеспечивает централизованное расположение для поддержания согласованности файлов Protobuf в приложениях, особенно в больших приложениях.
Для пользователей linux,
curl -sSL
https://github.com/uber/prototool/releases/download/v1.8.0/prototool-$(uname -s)-$(uname -m)
-o /usr/local/bin/prototool &&
chmod +x /usr/local/bin/prototool
или просто скачайте prototool-Linux-x86_64.tar.gz и затем распакуйте двоичный файл, поместив его в папку /usr/local/bin, чтобы сделать его исполняемым
Проверьте версию, чтобы убедиться в успешности установки:
mykmyk@skynet:~$ prototool version
Version: 1.9.0
Default protoc version: 3.8.0
Go version: go1.12.4
OS/Arch: linux/amd64
Погрузитесь глубже в тему prototool здесь.
Теперь давайте установим protoc…
Полное название — protobuf compiler, он используется для компиляции .proto файла. Погрузиться глубже можно здесь.
Для пользователей linux:
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
обновите переменную окружения PATH, чтобы помочь компилятору найти вышеупомянутый плагин «protoc-gen-go-grpc».
$ export PATH="$PATH:$(go env GOPATH)/bin"
GOPATH должен быть корневой папкой для бинарных файлов, расположенных в $GOPATH/bin, и должен быть корневой папкой для проектов.
Чтобы убедиться в успешности вышеуказанной установки, используйте следующую команду:
mykmyk@skynet:~$ protoc --version
libprotoc 3.15.8
Архитектура проекта
Готовый проект будет состоять из трех сервисов: двух микросервисов, которые мы построим в этой первой части, и сервиса-шлюза graphql, над которым мы будем работать во второй части этой серии.
Шлюз должен быть входом для всех клиентских запросов, связанных с велосипедами и их арендаторами, а два сервиса будут хранить свои собственные данные в ArangoDB. Когда арендатор арендует велосипед, арендатор и велосипед связаны идентификатором велосипеда. На основе этой связи мы можем получить ID арендатора, используя ID велосипеда.
Структура файлов проекта
Создайте корневую папку «bikerenting» с подпапками,
- gen для хранения нашего сгенерированного Go кода,
- Proto для хранения наших файлов proto,
- bikes для хранения нашей реализации API bikes,
- rentees для хранения нашей реализации API rentees и — graph_api для размещения нашей реализации graphl-шлюза.
- dockerfiles для целей контейнеризации.
mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting$ ll
total 32
drwxrwxr-x 8 mykmyk mykmyk 4096 Jun 1 23:18 ./
drwxrwxr-x 24 mykmyk mykmyk 4096 Jun 1 23:16 ../
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun 1 23:17 bikes/
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun 1 23:17 db/
-rw-rw-r-- 1 mykmyk mykmyk 0 Jun 1 23:18 docker-compose.yml
-rw-rw-r-- 1 mykmyk mykmyk 0 Jun 1 23:18 Dockerfile.dev
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun 1 23:17 gen/
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun 1 23:17 graph_api/
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun 1 23:17 proto/
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun 1 23:17 rentees/
Далее мы хотим создать конфигурационный файл prototool.yaml с помощью инструмента генерации кода prototool, который мы установили в начале этого проекта. Чтобы создать файл prototool.yaml в корневой папке проекта, выполним следующую команду:
mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting$ prototool config init
Вы заметите, что будет создан файл «prototool.yaml»,
1 protoc:
2 version: 3.8.0
3 lint:
4 group: uber2
теперь добавьте следующую информацию;
1 protoc:
2 version: 3.8.0
3 lint:
4 group: uber2
5
6 generate:
7 go_options:
8 import_path: bikerenting
9 plugins:
10 - name: go
11 type: go
12 flags: plugins=grpc
13 output: gen/go
В приведенном выше коде мы просто указываем пути для размещения/хранения нашего сгенерированного кода Go.
Создание нашего API для велосипедов и проката
Мы собираемся сгенерировать код, т.е. клиентские и серверные заглушки для обоих API. Сначала нам нужно определить структуры данных и операции над этими структурами данных в наших proto файлах для обоих API.
Давайте «cd» в корневую папку proto, которую мы создали выше, и создадим две новые папки для хранения наших соответствующих файлов proto, папку bikes и папку rentees, это будет выглядеть следующим образом.
mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting/proto$ ll
total 16
drwxrwxr-x 4 mykmyk mykmyk 4096 Jun 1 23:44 ./
drwxrwxr-x 8 mykmyk mykmyk 4096 Jun 1 23:36 ../
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun 1 23:44 bikes/
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun 1 23:44 rentees/
Теперь перейдите в папку «bikes» и создайте следующие файлы;
mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting/proto/bikes$ ll
total 8
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun 1 23:46 ./
drwxrwxr-x 4 mykmyk mykmyk 4096 Jun 1 23:44 ../
-rw-rw-r-- 1 mykmyk mykmyk 0 Jun 1 23:46 bikes_messages.proto
-rw-rw-r-- 1 mykmyk mykmyk 0 Jun 1 23:46 bikes.proto
откройте файл «bikes_messages.proto» и добавьте следующий код:
1 syntax = "proto3";
2
3 package bikerenting.grpc.bikes.v1;
4 option go_package = "bikes";
5
6 // Bike definition
7
8 message Bike {
9 string id = 1;
10 string owner_name = 2;
11 string type = 3;
12 string make = 4;
13 string serial = 5;
14 }
Затем откройте файл «bikes.proto» и добавьте следующий код:
syntax = "proto3";
package bikerenting.grpc.bikes.v1;
option go_package = "bikes";
import "proto/bikes/bikes_messages.proto";
//API for managing bikes
service BikesAPI {
//Get all bikes
rpc ListBikes(ListBikesRequest) returns (ListBikesResponse);
//Get bike by id
rpc GetBike(GetBikeRequest) returns (GetBikeResponse);
//Get bikes by ids
rpc GetBikes(GetBikesRequest) returns (GetBikesResponse);
// Get bikes by type
rpc GetBikesByTYPE(GetBikesByTYPERequest) returns (GetBikesByTYPEResponse);
// Get bikes by make
rpc GetBikesByMAKE(GetBikesByMAKERequest) returns (GetBikesByMAKEResponse);
// Get bikes by owner_name
rpc GetBikesByOWNER(GetBikesByOWNERRequest) returns (GetBikesByOWNERResponse);
// Add new bike
rpc AddBike(AddBikeRequest) returns (AddBikeResponse);
// Delete bike
rpc DeleteBike(DeleteBikeRequest) returns (DeleteBikeResponse);
}
message ListBikesRequest {
}
message ListBikesResponse {
repeated Bike bikes = 1;
}
message GetBikeRequest {
string id = 1;
}
message GetBikeResponse {
Bike bike = 1;
}
message GetBikesRequest {
repeated string ids = 1;
}
message GetBikesResponse {
repeated Bike bikes = 1;
}
message GetBikesByTYPERequest {
string type = 1;
}
message GetBikeByTYPEResponse {
repeated Bike bikes = 1;
}
message GetBikesByMAKERequest {
string make = 1;
}
message GetBikesByMAKEResponse {
repeated Bike bikes = 1;
}
message GetBikesByOWNERRequest {
string owner_name = 1;
}
message GetBikesByOWNERResponse {
repeated Bike bikes = 1;
}
message AddBikeRequest {
Bike bike = 1;
}
message AddBikeResponse {
Bike bike = 1;
}
message DeleteBikeRequest {
string id = 1;
}
message DeleteBikeResponse {
}
После определения двух файлов, один для хранения нашей структуры сообщений, а другой для определения операций над нашей структурой.
Далее выполните команды «go mod init» и «go mod tidy» в терминале, находясь в папке bikes.
В нашем коде выше
;
Proto3; (означает protocol buffers version 3) указывает, какой протокол мы используем для определения нашего API, генератор кода будет использовать эту версию для интерпретации наших определенных сообщений и операций.
option go_package = «bikes»; — В этом случае мы локально указываем путь импорта Go в этом .proto файле.
package package bikerenting.grpc.bikes.v1; — это позволяет генератору поместить сгенерированный код заглушки сервера-клиента bikes в это место.
Для наших арендаторов код будет иметь схожую структуру, откройте папку proto/rentees и создайте следующие файлы.
mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting/proto/rentees$ ll
total 8
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun 2 00:45 ./
drwxrwxr-x 4 mykmyk mykmyk 4096 Jun 1 23:44 ../
-rw-rw-r-- 1 mykmyk mykmyk 0 Jun 2 00:45 rentees_messages.proto
-rw-rw-r-- 1 mykmyk mykmyk 0 Jun 2 00:45 rentees.proto
Файл «rentees_messages.proto» будет выглядеть следующим образом:
1 syntax = "proto3";
2 package bikerenting.grpc.rentees.v1;
3 option go_package ="rentees";
4
5 // Customer definition
6 message Rentee {
7 string id = 1;
8 string first_name = 2;
9 string last_name = 3;
10 string National_Id_Number = 4;
11 string phone = 5;
12 string email = 6;
13 repeated string held_bikes = 7;
14 }
Файл «rentee.proto» будет выглядеть следующим образом:
syntax = "proto3";
package tutorial.grpc.rentees.v1;
option go_package = "rentees";
import "proto/rentees/rentees_messages.proto";
//API for managing rentees
service RenteesAPI {
//Get all rentees
rpc ListRentees(ListRenteesRequest) returns (ListRenteesResponse);
// Get rentee by bike id
rpc GetRenteeByBikeId(GetRenteeByBikeIdRequest) returns (GetRenteeByBikeIdResponse);
// Get rentee by bike type
rpc GetRenteesByBikeTYPE(GetRenteesByBikeTYPERequest) returns (GetRenteesByBikeTYPEResponse);
// Get rentee by bike make
rpc GetRenteesByBikeMAKE(GetRenteesByBikeMAKERequest) returns (GetRenteesByBikeMAKEResponse);
// Get rentee by bike owner
rpc GetRenteesByBikeOWNER(GetRenteesByBikeOWNERRequest) returns (GetRenteesByBikeOWNERResponse);
// Get rentee by id
rpc GetRentee(GetRenteeRequest) returns (GetRenteeResponse);
// Add new rentee
rpc AddRentee(AddRenteeRequest) returns (AddRenteeResponse);
// Update rentee
rpc UpdateRentee(UpdateRenteeRequest) returns (UpdateRenteeResponse);
}
message ListRenteesRequest {
}
message ListRenteesResponse {
repeated Rentee rentees = 1;
}
message GetRenteeByBikeIdRequest{
string id = 1;
}
message GetRenteeByBikeIdResponse {
Rentee rentee = 1;
}
message GetRenteesByBikeTYPERequest{
string type = 1;
}
message GetRenteeByBikeTYPEResponse {
repeated Rentee rentees = 1;
}
message GetRenteeByBikeMAKERequest{
string make = 1;
}
message GetRenteeByBikeMAKEResponse {
repeated Rentee rentees = 1;
}
message GetRenteeByBikeOWNERRequest{
string owner_name = 1;
}
message GetRenteeByBikeOWNERResponse {
repeated Rentee rentees = 1;
}
message GetRenteeRequest {
string id = 1 ;
}
message GetRenteeResponse {
Rentee rentee = 1;
}
message AddRenteeRequest {
Rentee rentee = 1;
}
message AddRenteeResponse {
Rentee rentee = 1;
}
message UpdateRenteeRequest {
Rentee rentee = 1;
}
message UpdateRenteeResponse {
Rentee rentee = 1;
}
После определения наших API rentees и bikes в наших .proto файлах, выполните команды «go mod init» и затем «go mod tidy». Теперь мы будем генерировать код для соответствующих клиент-серверных модулей, используя наш протоинструмент:
В нашей корневой папке, т.е. «bikerenting», где находится наш файл prototool.yaml, выполните следующую команду:
mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting$ prototool generate
Prototool найдет все наши протофайлы и использует конфигурацию prototool.yaml для создания заглушек наших клиент-серверных сервисных интерфейсов по пути, который мы указали для размещения этих файлов в соответствии с нашими конфигурациями в файле prototool.yaml. Теперь, если вы перейдете в папку «gen/go/proto». Мы обнаружим автогенерированные модули поддержки, оболочка для серверов API и клиента готова, и таким образом мы можем сосредоточиться на создании бизнес-логики или контроллера (или представления — Django) для наших двух API,
Папка «gen/go/proto» выглядит как :
mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting/gen/go/proto$ ll
total 16
drwxrwxr-x 4 mykmyk mykmyk 4096 Jun 2 01:28 ./
drwxr--r-- 3 mykmyk mykmyk 4096 Jun 2 01:28 ../
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun 2 01:28 bikes/
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun 2 01:28 rentees/
Теперь, если мы «cd» в папку bikes, мы найдем структуру ниже:
mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting/gen/go/proto/bikes$ ll
total 40
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun 2 01:28 ./
drwxrwxr-x 4 mykmyk mykmyk 4096 Jun 2 01:28 ../
-rw-rw-r-- 1 mykmyk mykmyk 2728 Jun 2 01:28 bikes_messages.pb.go
-rw-rw-r-- 1 mykmyk mykmyk 25373 Jun 2 01:28 bikes.pb.go
Если мы «cd» в папку rentees, мы найдем следующую структуру:
mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting/gen/go/proto/rentees$ ll
total 40
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun 2 01:28 ./
drwxrwxr-x 4 mykmyk mykmyk 4096 Jun 2 01:28 ../
-rw-rw-r-- 1 mykmyk mykmyk 3637 Jun 2 01:28 rentees_messages.pb.go
-rw-rw-r-- 1 mykmyk mykmyk 27424 Jun 2 01:28 rentees.pb.go
Вы можете заглянуть в этот файл, чтобы проверить сериализацию, которую выполнил grpc для наших сообщений:
Убедитесь, что вы запустили команды «go mod init» и «go mod tidy» как в папке rentees, так и в папке bikes.
Создание подключения к базе данных…..
Скачайте и установите базу данных Arangodb, на официальной странице здесь есть легкая инструкция по этому процессу (скачайте версию для сообщества).
PS: если вы столкнулись с ошибкой «
Setting up arangodb3 (3.2.10) ... FATAL ERROR: EXIT_FAILED - "exit with error" dpkg: error processing package arangodb3 (--configure): subprocess installed post-installation script returned error exit status 1 Errors were encountered while processing: arangodb3 E: Sub-process /usr/bin/dpkg returned an error code (1)
посмотрите мой ответ на эту ошибку на stack-overflow здесь
Чтобы убедиться, что база данных установлена правильно, используйте команду «arangosh», как показано ниже:
mykmyk@skynet:~$ arangosh
Please specify a password:
_
__ _ _ __ __ _ _ __ __ _ ___ ___| |__
/ _` | '__/ _` | '_ / _` |/ _ / __| '_
| (_| | | | (_| | | | | (_| | (_) __ | | |
__,_|_| __,_|_| |_|__, |___/|___/_| |_|
|___/
arangosh (ArangoDB 3.3.0 [linux] 64bit, using jemalloc, VPack 0.1.30, RocksDB 5.6.0, ICU 58.1, V8 5.7.492.77, OpenSSL 1.1.0f 25 May 2017)
Copyright (c) ArangoDB GmbH
Pretty printing values.
Connected to ArangoDB 'http+tcp://127.0.0.1:8529' version: 3.3.0 [server], database: '_system', username: 'root'
Please note that a new minor version '3.7.11' is available
Type 'tutorial' for a tutorial or 'help' to see common examples
127.0.0.1:8529@_system>
создайте новую базу данных и пользователя в оболочке arangosh следующим образом:
127.0.0.1:8529@_system> db._createDatabase("bikesrentees_db");
true
127.0.0.1:8529@_system> var users = require("@arangodb/users");
127.0.0.1:8529@_system> users.save("root@bikesrentees","rootpassword");
{
"user" : "root@bikesrentees",
"active" : true,
"extra" : {
},
"code" : 201
}
127.0.0.1:8529@_system> users.grantDatabase("root@bikesrentees", "bikesrentees_db");
127.0.0.1:8529@_system>
После создания базы данных и пользователя, сохраните учетные данные и информацию и из нашей корневой папки перейдите в папку «db», в папке db, что означает database, создайте два файла, connect.go и query.go,
Теперь эта папка будет выглядеть следующим образом:
mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting/db$ ll
total 8
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun 2 02:06 ./
drwxrwxr-x 8 mykmyk mykmyk 4096 Jun 1 23:36 ../
-rw-rw-r-- 1 mykmyk mykmyk 0 Jun 2 02:06 connect.go
-rw-rw-r-- 1 mykmyk mykmyk 0 Jun 2 02:06 query.go
Файл connect.go будет содержать код, который мы будем вызывать, когда захотим установить соединение с нашей базой данных arangodb, которую мы только что создали;
Код будет выглядеть как показано ниже, обратите внимание, что мы вставляем databasename, databaseuser и т.д:
package db
import (
"context"
"fmt"
"log"
"os"
"github.com/joho/godotenv"
"github.com/arangodb/go-driver"
"github.com/arangodb/go-driver/http"
)
func parseEnvVars(key string) string {
//load .env file
err := godotenv.Load(".env")
if err != nil {
log.Fatalf("Error loading .env file")
}
return os.Getenv(key)
}
const (
DbHost = "http://127.0.0.1"
DbPort = "8529"
DbUserName = "root@bikesrentees_db"
DbPassword = "rootpassword"
)
type DatabaseConfig struct {
Host string
Port string
Username string
Password string
DatabaseName string
}
func Connect(ctx context.Context, config DatabaseConfig)(db driver.Database, err error){
conn, err := http.NewConnection(http.ConnectionConfig{
Endpoints: []string{fmt.Sprintf("%s:%s", config.Host, config.Port)},
})
if err != nil {
return nil, err
}
cl, err := driver.NewClient(driver.ClientConfig{
Connection: conn,
Authentication: driver.BasicAuthentication(config.Username, config.Password),
})
if err != nil {
return nil, err
}
db, err = cl.Database(ctx, config.DatabaseName)
if driver.IsNotFound(err) {
db, err = cl.CreateDatabase(ctx, config.DatabaseName, nil)
}
return db, err
}
func AttachCollection(ctx context.Context, db driver.Database, colName string)(driver.Collection, error){
col, err := db.Collection(ctx, colName)
if err != nil {
if driver.IsNotFound(err){
col, err = db.CreateCollection(ctx, colName, nil)
}
}
return col, err
}
func GetDbConfig() DatabaseConfig{
dbName := parseEnvVars("ARANGODB_DB")
if dbName == "" {
log.Fatalf("Failed to load environment variable '%s'", "ARANGODB_DB")
}
return DatabaseConfig {
Host: DbHost,
Port: DbPort,
Username: DbUserName,
Password: DbPassword,
DatabaseName: dbName,
}
}
Далее откройте наш файл query.go и добавьте следующий код
package db
import "fmt"
func ListRecords(collectionName string) string {
const listRecordsQuery = `
FOR record IN %s
RETURN record`
return fmt.Sprintf(listRecordsQuery, collectionName)
}
Не забудьте выполнить ставшую уже монотонной пару команд «go mod init» и «go mod tidy» в папке db.
Теперь, когда у нас есть подключение к базе данных и логика запросов, давайте, наконец, создадим наши серверы rentees и bikes.
Из корня папки проекта перейдите в папку bikes, создайте папку server и файл main.go, папка будет выглядеть как показано ниже:
mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting/bikes$ ll
total 12
drwxrwxr-x 3 mykmyk mykmyk 4096 Jun 2 02:19 ./
drwxrwxr-x 8 mykmyk mykmyk 4096 Jun 1 23:36 ../
-rw-rw-r-- 1 mykmyk mykmyk 0 Jun 2 02:19 main.go
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun 2 02:19 server/
«cd» в только что созданную папку server и создайте файл под названием «server.go».
Откройте файл server.go с помощью вашего любимого текстового редактора и добавьте следующий код:
package server
import (
"context"
"fmt"
"log"
"net"
"os"
"github.com/myk4040okothogodo/bikerenting/db"
bikesv1 "github.com/myk4040okothogodo/bikerenting/gen/go/proto/bikes"
"github.com/arangodb/go-driver"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
const (
bikesCollectionName = "Bikes"
defaultPort = "60001"
)
type Server struct {
database driver.Database
bikesCollection driver.Collection
}
func NewServer(ctx context.Context, database driver.Database)(*Server, error) {
collection, err := db.AttachCollection(ctx, database, bikesCollectionName)
if err != nil {
return nil, err
}
_, _, err = collection.EnsurePersistentIndex(ctx, []string{"serial"}, &driver.EnsurePersistentIndexOptions{Unique: true})
if err != nil {
return nil, err
}
return &Server{
database: database,
bikesCollection: collection,
}, nil
}
func (s *Server) Run() {
port := os.Getenv("APP_PORT")
if port == "" {
port = defaultPort
}
listener,err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%s", port))
if err != nil {
log.Fatal("net.Listen failed")
return
}
grpcServer := grpc.NewServer()
bikesv1.RegisterBikesAPIServer(grpcServer, s)
reflection.Register(grpcServer)
log.Printf("Starting Rental Bikes server on port %s", port)
go func() {
grpcServer.Serve(listener)
}()
}
func (s *Server) ListBikes(ctx context.Context, in *bikesv1.ListBikesRequest)(*bikesv1.ListBikesResponse, error){
if in == nil {
return nil, fmt.Errorf("Request is empty")
}
cursor, err := s.database.Query(ctx, db.ListRecords(s.bikesCollection.Name()), nil)
if err != nil {
return nil, fmt.Errorf("Failed to iterate over documents: %s", err)
}
defer cursor.Close()
allBikes := []*bikesv1.Bike{}
for {
bike := new(bikesv1.Bike)
var meta driver.DocumentMeta
meta, err := cursor.ReadDocument(ctx, bike)
if driver.IsNoMoreDocuments(err){
break
} else if err != nil {
return nil, fmt.Errorf("Failed to read bike document: %s", err)
}
bike.Id = meta.Key
allBikes = append(allBikes, bike)
}
return &bikesv1.ListBikesResponse{Bikes: allBikes}, nil
}
func (s *Server) GetBike(ctx context.Context, in *bikesv1.GetBikeRequest)(*bikesv1.GetBikeResponse, error){
if in == nil || in.Id == "" {
return nil, fmt.Errorf("Bike id is not provided")
}
bike := new(bikesv1.Bike)
meta, err := s.bikesCollection.ReadDocument(ctx, in.Id, bike)
if err != nil {
if driver.IsNotFound(err){
err = fmt.Errorf("Bike with id '%s' not found", in.Id)
} else {
err = fmt.Errorf("Failed to get bike with id '%s'", in.Id, err)
}
return nil, err
}
bike.Id = meta.Key
return &bikesv1.GetBikeResponse{Bike: bike}, nil
}
func (s *Server) GetBikes(ctx context.Context, in *bikesv1.GetBikesRequest) (*bikesv1.GetBikesResponse, error){
if in == nil || len(in.Ids) == 0 {
return nil, fmt.Errorf("Bikes ids are not provided")
}
const queryBikesByIds = `
FOR bike IN %s
FILTER bike._key in @ids
RETURN bike`
query := fmt.Sprintf(queryBikesByIds, bikesCollectionName)
bindVars := map[string]interface{}{"ids":in.Ids}
cursor, err := s.database.Query(ctx, query, bindVars)
if err != nil {
return nil, fmt.Errorf("Failed to iterate over bike docments with query '%s': %s", queryBikesByIds, err)
}
defer cursor.Close()
bikes := []*bikesv1.Bike{}
for {
bike := new(bikesv1.Bike)
meta, err := cursor.ReadDocument(ctx, bike)
if driver.IsNoMoreDocuments(err) {
break
} else if err != nil {
log.Print(err)
return nil, fmt.Errorf("Failed to read bike document: %s", err)
}
bike.Id = meta.Key
bikes = append(bikes, bike)
}
return &bikesv1.GetBikesResponse{Bikes: bikes}, nil
}
func (s *Server) GetBikesByTYPE(ctx context.Context, in *bikesv1.GetBikesByTYPERequest)(*bikesv1.GetBikesByTYPEResponse, error){
if in == nil || in.Type == "" {
return nil, fmt.Errorf("Bike type is not provided")
}
const queryBikeByTYPE = `
FOR bike IN %s
FILTER bike.type == @type
RETURN bike`
query := fmt.Sprintf(queryBikeByTYPE, bikesCollectionName)
bindVars := map[string]interface{}{"type": in.Type}
cursor, err := s.database.Query(ctx, query, bindVars)
if err != nil {
return nil, fmt.Errorf("Failed to iterate over bike documents with query '%s': %s", queryBikeByTYPE, err)
}
defer cursor.Close()
bikes := []*bikesv1.Bike{}
for {
bike := new(bikesv1.Bike)
meta, err := cursor.ReadDocument(ctx, bike)
if driver.IsNoMoreDocuments(err){
break
} else if err != nil {
log.Print(err)
return nil, fmt.Errorf("failed to read rentees document: %s", err)
}
bike.Id = meta.Key
bikes = append(bikes, bike)
}
return &bikesv1.GetBikesByTYPEResponse{Bikes: bikes}, nil
}
func (s *Server) GetBikesByOWNER(ctx context.Context, in *bikesv1.GetBikesByOWNERRequest)(*bikesv1.GetBikesByOWNERResponse, error){
if in == nil || in.OwnerName == "" {
return nil, fmt.Errorf("Bike owner is not provided")
}
const queryBikeByOWNER = `
FOR bike IN %s
FILTER bike.owner_name == @ownername
RETURN bike`
query := fmt.Sprintf(queryBikeByOWNER, bikesCollectionName)
bindVars := map[string]interface{}{"owner_name": in.OwnerName}
cursor, err := s.database.Query(ctx, query, bindVars)
if err != nil {
return nil, fmt.Errorf("Failed to iterate over bike documents with query '%s': %s", queryBikeByOWNER, err)
}
defer cursor.Close()
bikes := []*bikesv1.Bike{}
for {
bike := new(bikesv1.Bike)
meta, err := cursor.ReadDocument(ctx, bike)
if driver.IsNoMoreDocuments(err){
break
} else if err != nil {
log.Print(err)
return nil, fmt.Errorf("failed to read rentees document: %s", err)
}
bike.Id = meta.Key
bikes = append(bikes, bike)
}
return &bikesv1.GetBikesByOWNERResponse{Bikes: bikes}, nil
}
func (s *Server) GetBikesByMAKE(ctx context.Context, in *bikesv1.GetBikesByMAKERequest)(*bikesv1.GetBikesByMAKEResponse, error){
if in == nil || in.Make == "" {
return nil, fmt.Errorf("Bike make is not provided")
}
const queryBikeByMAKE = `
FOR bike IN %s
FILTER bike.make == @make
RETURN bike`
query := fmt.Sprintf(queryBikeByMAKE, bikesCollectionName)
bindVars := map[string]interface{}{"make": in.Make}
cursor, err := s.database.Query(ctx, query, bindVars)
if err != nil {
return nil, fmt.Errorf("Failed to iterate over bike documents with query '%s': %s", queryBikeByMAKE, err)
}
defer cursor.Close()
bikes := []*bikesv1.Bike{}
for {
bike := new(bikesv1.Bike)
meta, err := cursor.ReadDocument(ctx, bike)
if driver.IsNoMoreDocuments(err){
break
} else if err != nil {
log.Print(err)
return nil, fmt.Errorf("failed to read rentees document: %s", err)
}
bike.Id = meta.Key
bikes = append(bikes, bike)
}
return &bikesv1.GetBikesByMAKEResponse{Bikes: bikes}, nil
}
//func (s *Server) GetBikeBySERIAL(ctx context.Context, in *bikesv1.GetBikesSERIALRequest)(*bikesv1.GetBikesBySERIALResponse, error){
// if in == nil || in.Serial == "" {
// return nil, fmt.Errorf("Bike serial is not provided")
// }
// const queryBikeBySERIAL = `
// FOR bike IN %s
// FILTER bike.serial == @serial
// RETURN bike`
// query := fmt.Sprintf(queryBikeBySERIAL, bikesCollectionName)
// bindVars := map[string]interface{}{"serial": in.Serial}
// cursor, err := s.database.Query(ctx, query, bindVars)
// if err != nil {
// return nil, fmt.Errorf("Failed to iterate over bike documents with query '%s': %s", queryBikeBySERIAL, err)
// }
// defer cursor.Close()
// b := new(bikesv1.Bike)
// meta, err := cursor.ReadDocument(ctx, b)
// if driver.IsNoMoreDocuments(err){
// return nil, fmt.Errorf("Bike with SERIAL '%s' not found: %s", in.Serial, err)
// } else if err != nil {
// return nil, fmt.Errorf("Failed to read bike document: %s", err)
// }
// b.Id = meta.Key
// return &bikesv1.GetBikesBySERIALResponse{Bike: b}, nil
//}
func (s *Server) AddBike(ctx context.Context, in *bikesv1.AddBikeRequest)(*bikesv1.AddBikeResponse, error){
if in == nil || in.Bike == nil {
return nil, fmt.Errorf("Bike is not provided")
}
meta, err := s.bikesCollection.CreateDocument(ctx, in.Bike)
if err != nil {
return nil, fmt.Errorf("failed to create bike: %s", err)
}
in.Bike.Id = meta.Key
return &bikesv1.AddBikeResponse{Bike: in.Bike}, nil
}
func (s *Server) DeleteBike(ctx context.Context, in *bikesv1.DeleteBikeRequest)(*bikesv1.DeleteBikeResponse, error) {
if in == nil || in.Id == "" {
return nil, fmt.Errorf("Bike id is not provided")
}
_, err := s.bikesCollection.RemoveDocument(ctx, in.Id)
if err != nil {
return nil, fmt.Errorf("Failed to remove existing bike: %s", err)
}
return &bikesv1.DeleteBikeResponse{}, nil
}
}
Запустите команды «go mod init» и «go mod tidy».
Все, что мы делаем в приведенном выше коде, это реализуем логику для перечисления велосипедов, используя их идентификаторы, марки, типы и серийные номера, мы также реализуем логику для добавления нового велосипеда в систему и удаления велосипеда из системы, Теперь, чтобы вызвать этот сервер, создав новый сервер, вернитесь на один уровень назад «cd …» и откройте файл «main.go» и добавьте следующий код.
1 package main
2
3 import (
4 "context"
5 "log"
6 "os"
7 "os/signal"
8 "syscall"
9 "time"
10 "github.com/myk4040okothogodo/bikerenting/bikes/server"
11 "github.com/myk4040okothogodo/bikerenting/db"
12 )
13
14
15 func main(){
16 ctx, cancelFn := context.WithTimeout(context.Background(), time.Second*5)
17 defer cancelFn()
18
19 database, err := db.Connect(ctx, db.GetDbConfig())
20 if err != nil {
21 log.Fatalf("d.OpenDatabase failed with error: %s", err)
22 }
23
24 srv, err := server.NewServer(ctx, database)
25 if err != nil {
26 log.Fatalf("NewServer failed with error: %s", err)
27 }
28
29 srv.Run()
30
31 sigChan := make(chan os.Signal, 1)
32 signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
33 signal := <-sigChan
34 log.Printf("shutting down bikes server with signal: %s", signal)
35
36 }
В приведенном выше коде мы соединяемся с нашей базой данных в соответствии с конфигурацией, которую мы предоставили в файле «db/connect.go», затем создаем новый сервер в соответствии с нашим определением в файле «server.go» и запускаем этот сервер, помните, что этот сервер будет Go-программой, поэтому мы используем канал для прослушивания любых сигналов уничтожения сервера, т.е. «SIGTERM», чтобы убить наш сервер.
Мы повторим приведенный выше код для создания нашего API rentees, теперь перейдите в папку rentees из корневой папки проекта и добавьте следующий код, структура файла будет идентична той, которую мы только что создали выше, поэтому, чтобы избежать повторений, я не буду повторять объяснения.
mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting/rentees$ ll
total 12
drwxrwxr-x 3 mykmyk mykmyk 4096 Jun 2 10:49 ./
drwxrwxr-x 9 mykmyk mykmyk 4096 Jun 2 02:44 ../
-rw-rw-r-- 1 mykmyk mykmyk 0 Jun 2 10:48 main.go
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun 2 10:49 server/
для нашего «bikerenting/rentees/server/server.go»
package server
import (
"context"
"fmt"
"log"
"net"
"os"
"github.com/myk4040okothogodo/bikerenting/db"
renteesv1 "github.com/myk4040okothogodo/bikerenting/gen/go/proto/rentees"
"github.com/arangodb/go-driver"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
const (
renteesCollectionName = "rentees"
defaultPort = "60002"
)
type Server struct {
database driver.Database
renteesCollection driver.Collection
}
func NewServer(ctx context.Context, database driver.Database)(*Server, error){
collection, err := db.AttachCollection(ctx, database, renteesCollectionName)
if err != nil {
return nil, err
}
return &Server {
database: database,
renteesCollection: collection,
}, nil
}
func (s *Server) Run() {
port := os.Getenv("APP_PORT")
if port == "" {
port = defaultPort
}
listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0: %s", port))
if err != nil {
log.Print("net.Listen failed")
return
}
grpcServer := grpc.NewServer()
renteesv1. RegisterRenteesAPIServer(grpcServer, s) // use autogenerated code to register the server
reflection.Register(grpcServer)
log.Printf("Starting Rentees server on port %s", port)
go func() {
grpcServer.Serve(listener)
}()
}
func (s *Server) ListRentees(ctx context.Context, in *renteesv1.ListRenteesRequest)(*renteesv1.ListRenteesResponse, error){
if in == nil {
return nil, fmt.Errorf("Request is empty")
}
cursor, err := s.database.Query(ctx, db.ListRecords(s.renteesCollection.Name()), nil)
if err != nil {
return nil, fmt.Errorf("failed to iterate over documents: %s", err)
}
defer cursor.Close()
allRentees := []*renteesv1.Rentee{}
for {
rentee := new(renteesv1.Rentee)
var meta driver.DocumentMeta
meta, err := cursor.ReadDocument(ctx, rentee)
if driver.IsNoMoreDocuments(err) {
break
} else if err != nil {
return nil, fmt.Errorf("Failed to read rentee document: %s", err)
}
rentee.Id = meta.Key
allRentees = append(allRentees, rentee)
}
return &renteesv1.ListRenteesResponse{Rentees: allRentees}, nil
}
func (s *Server) GetRentee(ctx context.Context, in *renteesv1.GetRenteeRequest)(*renteesv1.GetRenteeResponse, error) {
if in == nil || in.Id == "" {
return nil, fmt.Errorf("Rentee id is not provided")
}
rentee := new(renteesv1.Rentee)
meta, err := s.renteesCollection.ReadDocument(ctx, in.Id, rentee)
if err != nil {
if driver.IsNotFound(err) {
err = fmt.Errorf("Rentee with id '%s' not found", in.Id)
} else {
err = fmt.Errorf("Failed to get rentee with id '%s':'%s'", in.Id, err)
}
return nil, err
}
rentee.Id = meta.Key
return &renteesv1.GetRenteeResponse{Rentee: rentee}, nil
}
func (s *Server) GetRenteeByBikeId(ctx context.Context, in *renteesv1.GetRenteeByBikeIdRequest)(*renteesv1.GetRenteeByBikeIdResponse, error){
if in == nil || in.Id == "" {
return nil, fmt.Errorf("Bike id is not provided")
}
const queryRenteeByBikeId = `
FOR rentee IN %s
FOR bikeId IN rentee.held_bikes
FILTER bikeId == @bikeId
RETURN rentee`
query := fmt.Sprintf(queryRenteeByBikeId, renteesCollectionName)
bindVars := map[string]interface{}{"bikeId": in.Id}
cursor, err := s.database.Query(ctx, query, bindVars)
if err != nil {
return nil, fmt.Errorf("Failed to iterate over rentee documents with query '%s': %s", queryRenteeByBikeId, err)
}
defer cursor.Close()
r := new(renteesv1.Rentee)
meta, err := cursor.ReadDocument(ctx, r)
if driver.IsNoMoreDocuments(err){
return nil, fmt.Errorf("Rentee that held bike with id %s not found: %s", in.Id, err)
} else if err != nil {
return nil, fmt.Errorf("Failed to read rentee document: %s", err)
}
r.Id = meta.Key
return &renteesv1.GetRenteeByBikeIdResponse{Rentee: r}, nil
}
func (s *Server) GetRenteesByBikeTYPE(ctx context.Context, in *renteesv1.GetRenteesByBikeTYPERequest)(*renteesv1.GetRenteesByBikeTYPEResponse, error){
if in == nil || in.Type == " " {
return nil, fmt.Errorf("Request is empty")
}
const queryRenteeByBikeTYPE = `
FOR rentee IN %s
FOR bikeType IN rentee.held_bikes
FILTER bikeType == @type
RETURN rentee`
query := fmt.Sprintf(queryRenteeByBikeTYPE, renteesCollectionName)
bindVars := map[string]interface{}{"bikeType": in.Type}
cursor, err := s.database.Query(ctx, query, bindVars)
if err != nil {
return nil, fmt.Errorf("Failed to iterate over rentee documents with query '%s': %s", queryRenteeByBikeTYPE, err)
}
defer cursor.Close()
rentees := []*renteesv1.Rentee{}
for {
rentee := new(renteesv1.Rentee)
meta, err := cursor.ReadDocument(ctx, rentee)
if driver.IsNoMoreDocuments(err){
break
} else if err != nil {
log.Print(err)
return nil, fmt.Errorf("failed to read rentees document: %s", err)
}
rentee.Id = meta.Key
rentees = append(rentees, rentee)
}
return &renteesv1.GetRenteesByBikeTYPEResponse{Rentees: rentees}, nil
}
func (s *Server) GetRenteesByBikeMAKE(ctx context.Context, in *renteesv1.GetRenteesByBikeMAKERequest)(*renteesv1.GetRenteesByBikeMAKEResponse, error){
if in == nil || in.Make == " " {
return nil, fmt.Errorf("Request is empty")
}
const queryRenteeByBikeMAKE = `
FOR rentee IN %s
FOR bikeMake IN rentee.held_bikes
FILTER bikeMake == @make
RETURN rentee`
query := fmt.Sprintf(queryRenteeByBikeMAKE, renteesCollectionName)
bindVars := map[string]interface{}{"bikeMake": in.Make}
cursor, err := s.database.Query(ctx, query, bindVars)
if err != nil {
return nil, fmt.Errorf("Failed to iterate over rentee documents with query '%s': %s", queryRenteeByBikeMAKE, err)
}
defer cursor.Close()
rentees := []*renteesv1.Rentee{}
for {
rentee := new(renteesv1.Rentee)
meta, err := cursor.ReadDocument(ctx, rentee)
if driver.IsNoMoreDocuments(err){
break
} else if err != nil {
log.Print(err)
return nil, fmt.Errorf("failed to read rentees document: %s", err)
}
rentee.Id = meta.Key
rentees = append(rentees, rentee)
}
return &renteesv1.GetRenteesByBikeMAKEResponse{Rentees: rentees}, nil
}
func (s *Server) GetRenteesByBikeOWNER(ctx context.Context, in *renteesv1.GetRenteesByBikeOWNERRequest)(*renteesv1.GetRenteesByBikeOWNERResponse, error){
if in == nil || in.OwnerName == " " {
return nil, fmt.Errorf("Request is empty")
}
const queryRenteeByBikeOWNER = `
FOR rentee IN %s
FOR bikeOwner IN rentee.held_bikes
FILTER bikeOwner == @owner
RETURN rentee`
query := fmt.Sprintf(queryRenteeByBikeOWNER, renteesCollectionName)
bindVars := map[string]interface{}{"bikeOwner": in.OwnerName}
cursor, err := s.database.Query(ctx, query, bindVars)
if err != nil {
return nil, fmt.Errorf("Failed to iterate over rentee documents with query '%s': %s", queryRenteeByBikeOWNER, err)
}
defer cursor.Close()
rentees := []*renteesv1.Rentee{}
for {
rentee := new(renteesv1.Rentee)
meta, err := cursor.ReadDocument(ctx, rentee)
if driver.IsNoMoreDocuments(err){
break
} else if err != nil {
log.Print(err)
return nil, fmt.Errorf("failed to read rentees document: %s", err)
}
rentee.Id = meta.Key
rentees = append(rentees, rentee)
}
return &renteesv1.GetRenteesByBikeOWNERResponse{Rentees: rentees}, nil
}
func (s *Server) AddRentee(ctx context.Context, in *renteesv1.AddRenteeRequest) (*renteesv1.AddRenteeResponse, error) {
if in == nil || in.Rentee == nil {
return nil, fmt.Errorf("Rentee is not provided")
}
meta, err := s.renteesCollection.CreateDocument(ctx, in.Rentee)
if err != nil {
return nil, fmt.Errorf("Failed to create rentee: %s", err)
}
in.Rentee.Id = meta.Key
return &renteesv1.AddRenteeResponse{Rentee: in.Rentee}, nil
}
func (s *Server) UpdateRentee(ctx context.Context, in *renteesv1.UpdateRenteeRequest)(*renteesv1.UpdateRenteeResponse, error){
if in == nil || in.Rentee == nil || in.Rentee.Id == "" {
return nil, fmt.Errorf("Existing rentee is provided")
}
_, err := s.renteesCollection.ReplaceDocument(ctx, in.Rentee.Id, in.Rentee)
if err != nil {
return nil, fmt.Errorf("Failed to update with id %s", in.Rentee.Id, err)
}
return &renteesv1.UpdateRenteeResponse{Rentee: in.Rentee}, nil
}
Для нашего файла «bikerenting/rentees/main.go» мы добавим в него следующий код.
1 package main
2
3 import (
4 "context"
5 "log"
6 "os"
7 "os/signal"
8 "syscall"
9 "time"
10 "github.com/myk4040okothogodo/bikerenting/db"
11 "github.com/myk4040okothogodo/bikerenting/rentees/server"
12 )
13
14
15 func main() {
16 ctx, cancelFn := context.WithTimeout(context.Background(), time.Second*5)
17 defer cancelFn()
18
19 database, err := db.Connect(ctx, db.GetDbConfig())
20 if err != nil {
21 log.Fatalf("db.OpenDatabase failed with error: %s", err)
22 }
23
24 srv, err := server.NewServer(ctx, database)
25 if err != nil {
26 log.Fatalf("NewServer failed with error: %s", err)
27 }
28
29 srv.Run()
30
31 sigChan := make(chan os.Signal, 1)
32 signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
33 signal := <-sigChan
34 log.Printf("shutting down Rentees servers with signal: %s", signal)
35 }
Запустите команды «go mod init» и «go mod tidy».
Статья получилась слишком большой, поэтому мне придется создать «интерлюдию» перед второй частью. В этой статье речь пойдет о написании тестов для нашего API, а также об использовании grpcurl для тестирования конечных точек. До встречи.