有很多文章介紹瞭如何使用幾個優秀的Web框架和/或routers建立Go REST微服務。當我為公司尋找最佳方法時,我閱讀了大部分內容。突然間,我發現了另一種非常有趣的方法來開發HTTP / REST微服務。它是來自Google的protobuf / gRPC框架。我相信大家都知道。有人已經使用過gRPC。但我相信沒有那麼多人有使用protobuf / gRPC開發HTTP / REST微服務的經驗。我發現只有一篇實際的 Medium 的文章:
https://medium.com/@thatcher/why-choose-between-grpc-and-rest-bc0d351f2f84
我不打算重覆這篇這麼棒的文章。我想提供一個step by step的教程,教你如何使用gRPC和HTTP / REST端點開發簡單的具有增刪改查(CRUD)功能的“待辦事項串列”微服務。我演示瞭如何編寫測試並將中介軟體(請求ID和日誌記錄 / 鏈路跟蹤)新增到微服務中。並且我還提供瞭如何在最後構建和部署這個微服務到Kubernetes的示例。
內容串列
教程由4部分組成:
- 第1部分是關於如何建立gRPC CRUD服務和客戶端
- 第2部分是關於如何將HTTP / REST端點新增到gRPC服務
- 第3部分是關於如何向gRPC服務和HTTP / REST端點新增中介軟體(例如,日誌記錄 / 鏈路跟蹤)
- 第4部分將專門介紹如何新增Kubernetes部署配置並且進行執行狀態檢查以及如何構建專案並將其部署到Google Cloud
前提條件
- 本文不是Go語言的培訓材料。我假設你已經有了一些經驗。
- 您必須在開始之前安裝並配置Go 1.11。我們將使用Go Modules功能。
- 您必須具備如何安裝/配置任何SQL資料庫以將其用作本教程的持久儲存的經驗。
API 先行
這對我意味著什麼?
- API定義必須是語言,協議,傳輸中立。
- API定義和API實現必須鬆散耦合。
- API版本控制。
- 我需要排除手動工作以同步API定義,API實現和API檔案。我需要API實現存根/骨架和API檔案自動從API定義生成。
我會在課程中強調了這一點。
“待辦事項串列”微服務
“待辦事項串列”微服務允許管理“To Do”專案。ToDo項包含以下欄位:
- ID (unique integer identifier)
- Title (text)
- Description (text)
- Reminder (timestamp)
ToDo服務還包含經典的CRUD方法如Create,Read,Update,Delete和ReadAll。
Part 1:建立gRPC CRUD服務
Step 1:建立API定義
第1部分的原始碼可在此處獲得:https://github.com/amsokol/go-grpc-http-rest-microservice-tutorial
在開始之前,我們需要建立Go專案結構。這裡有一個很棒的Go專案模板: https://github.com/golang-standards/project-layout。請像我一樣使用它!
我使用的是Windows 10 x64環境。不過我認為將CMD命令轉換為MacOS / Linux BASH並不是問題。(譯者註:我使用的是Ubuntu)
首先建立並輸入根專案檔案夾go-grpc-http-rest-microservice-tutorial(在GOPATH之外找到它以使用Go模組)。比初始化Go專案:
mkdir go-grpc-http-rest-microservice-tutorial
cd go-grpc-http-rest-microservice-tutorial
go mod init github.com//go-grpc-http-rest-microservice-tutorial
為API定義建立檔案夾結構:
mkdir -p api/proto/v1
其中v1是API版本。
API版本控制:我的最佳做法是在不同的檔案夾中找到主要版本的API。
接下來在api/proto/v1檔案夾中建立todo-service.proto檔案,並使用一個方法 Create 新增ToDoService的定義為開頭:
syntax = "proto3";
package v1;
import "google/protobuf/timestamp.proto";
// 用於管理待辦事項串列的服務
service ToDoService {
// 建立新的待辦事項任務
rpc Create (CreateRequest) returns (CreateResponse) {}
}
message CreateRequest {
// API版本控制:這是明確指定版本的最佳實踐
string api = 1;
// 要新增的任務物體
ToDo toDo = 2;
}
message ToDo {
// 待辦事項任務的唯一整數識別符號
int64 id = 1;
// 任務的標題
string title = 2;
// 待辦事項任務的詳細說明
string description = 3;
// 提醒待辦任務的日期和時間
google.protobuf.Timestamp reminder = 4;
}
message CreateResponse {
// API版本控制:這是明確指定版本的最佳實踐
string api = 1;
// 已建立任務的ID
int64 id = 2;
}
你可以在這裡獲得proto語言規範:https://developers.google.com/protocol-buffers/docs/proto3
正如您所看到的,我們的API定義絕對是語言,協議,傳輸中立。這是protobuf的關鍵特性之一。
要編譯Proto檔案,我們需要安裝必要的工具並新增包。
-
在這裡下載Proto編譯器二進位制檔案:https://github.com/protocolbuffers/protobuf/releases
-
將包解壓縮到PC上的任何檔案夾,並將“bin”目錄新增到PATH環境變數中
-
在“go-grpc-http-rest-microservice-tutorial”中建立“third_party”檔案夾
-
將所有內容從Proto編譯器“include”檔案夾複製到“third_party”檔案夾(這個檔案夾自己去建立)
-
為Proto編譯器安裝Go語言程式碼生成器外掛:
go get -u github.com/golang/protobuf/protoc-gen-go
-
在“third_party”檔案夾中建立protoc-gen.cmd(MacOS / Linux的protoc-gen.sh)檔案:
protoc --proto_path=api/proto/v1 --proto_path=third_party --go_out=plugins=grpc:pkg/api/v1 todo-service.proto
-
為生成的Go檔案建立輸出檔案夾:
mkdir -p pkg/api/v1
-
確保我們在go-grpc-http-rest-microservice-tutorial檔案夾中並執行編譯:
.\third_party\protoc-gen.cmd
對於 MacOS / Linux:
./third_party/protoc-gen.sh
它在“pkg / model / v1”檔案夾中建立todo-service.pb.go檔案。
很棒。讓我們新增剩餘的ToDo服務方法並編譯:
syntax = "proto3";
package v1;
import "google/protobuf/timestamp.proto";
// 用於管理待辦事項串列的服務
service ToDoService {
// 建立新的待辦事項任務
rpc Create (CreateRequest) returns (CreateResponse) {}
// 讀取待辦事項任務
rpc Read(ReadRequest) returns (ReadResponse) {}
// 更新待辦事項任務
rpc Update(UpdateRequest) returns (UpdateResponse) {}
// 刪除待辦事項任務
rpc Delete(DeleteRequest) returns (DeleteResponse) {}
// 讀取全部待辦事項任務
rpc ReadAll(ReadAllRequest) returns (ReadAllResponse) {}
}
// 請求資料以建立新的待辦事項任務
message CreateRequest {
// API版本控制:這是明確指定版本的最佳實踐
string api = 1;
// 要新增的任務物體
ToDo toDo = 2;
}
// 我們要做的是Task
message ToDo {
// 待辦事項任務的唯一整數識別符號
int64 id = 1;
// 任務的標題
string title = 2;
// 待辦事項任務的詳細說明
string description = 3;
// 提醒待辦任務的日期和時間
google.protobuf.Timestamp reminder = 4;
}
// 包含建立的待辦事項任務的資料
message CreateResponse {
// API版本控制:這是明確指定版本的最佳實踐
string api = 1;
// 已建立任務的ID
int64 id = 2;
}
// 求資料讀取待辦事項任務
message ReadRequest {
// API版本控制:這是明確指定版本的最佳實踐
string api = 1;
// 待辦事項任務的唯一整數識別符號
int64 id = 2;
}
// 包含ID請求中指定的待辦事項任務資料
message ReadResponse {
// API版本控制:這是明確指定版本的最佳實踐
string api = 1;
// 按ID讀取的任務物體
ToDo toDo = 2;
}
// 請求資料以更新待辦事項任務
message UpdateRequest {
// API版本控制:這是明確指定版本的最佳實踐
string api = 1;
// 要更新的任務物體
ToDo toDo = 2;
}
// 包含更新操作的狀態
message UpdateResponse {
// API版本控制:這是明確指定版本的最佳實踐
string api = 1;
// 包含已更新的物體數量
// 在成功更新的情況下等於1
int64 updated = 2;
}
// 請求資料刪除待辦事項任務
message DeleteRequest {
// API版本控制:這是明確指定版本的最佳實踐
string api = 1;
// 要刪除的待辦事項任務的唯一整數識別符號
int64 id = 2;
}
// 包含刪除操作的狀態
message DeleteResponse {
// API版本控制:這是明確指定版本的最佳實踐
string api = 1;
// 包含已刪除的物體數量
// 成功刪除時等於1
int64 deleted = 2;
}
// 請求資料以讀取所有待辦事項任務
message ReadAllRequest {
// API版本控制:這是明確指定版本的最佳實踐
string api = 1;
}
// 包含所有待辦事項任務的串列
message ReadAllResponse {
// API版本控制:這是明確指定版本的最佳實踐
string api = 1;
repeated ToDo toDos = 2;
}
並再次執行proto編譯器來更新Go程式碼:
.\third_party\protoc-gen.cmd
對於 MacOS / Linux:
./third_party/protoc-gen.sh
你必須在現實生活中新增Go檔案生成作為CI / CD步驟,以避免手動執行。
OK。API定義已經準備好了。
Step2:使用Go語言開發API實現
我使用Google Cloud中的MySQL資料庫作為本教程的持久儲存。你也可以使用你喜歡的另一個SQL資料庫。
MySQL建立ToDo表的指令碼如下:
CREATE TABLE `ToDo` (
`ID` bigint(20) NOT NULL AUTO_INCREMENT,
`Title` varchar(200) DEFAULT NULL,
`Description` varchar(1024) DEFAULT NULL,
`Reminder` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`ID`),
UNIQUE KEY `ID_UNIQUE` (`ID`)
) ENGINE=InnoDB CHARSET=utf8mb4;
我在本教程中避免瞭如何安裝,配置SQL資料庫和建立表的步驟。
使用以下內容建立檔案“pkg / service / v1 / todo-service.go”:
package v1
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/golang/protobuf/ptypes"
"github.com/fengberlin/go-grpc-http-rest-microservice-tutorial/pkg/api/v1"
"google.golang.org/grpc/status"
"google.golang.org/grpc/codes"
)
const (
// apiVersion是由伺服器提供的API的版本
apiVersion = "v1"
)
// toDoServiceServer是v1.ToDoServiceServer proto介面的實現
type toDoServiceServer struct {
db *sql.DB
}
// NewToDoServiceServer建立ToDo服務
func NewToDoServiceServer(db *sql.DB) v1.ToDoServiceServer {
return &toDoServiceServer;{db}
}
// checkAPI檢查伺服器是否支援客戶端請求的API版本
func (s *toDoServiceServer) checkAPI(api string) error {
// API版本是“”表示使用當前版本的服務
if len(api) > 0 {
if apiVersion != api {
return status.Errorf(codes.Unimplemented,
"unsupported API version: service implements API version '%s', but asked for '%s'", apiVersion, api)
}
}
return nil
}
// connect 從池中傳回SQL資料庫連線
func (s *toDoServiceServer) connect(ctx context.Context) (*sql.Conn, error) {
c, err := s.db.Conn(ctx)
if err != nil {
return nil, status.Error(codes.Unknown, "failed to connect to database-> "+err.Error())
}
return c, nil
}
// 建立新的待辦事項任務
func (s *toDoServiceServer) Create(ctx context.Context, req *v1.CreateRequest) (*v1.CreateResponse, error) {
// 檢查伺服器是否支援客戶端請求的API版本
if err := s.checkAPI(req.Api); err != nil {
return nil, err
}
// 從池中獲取sql連線
c, err := s.connect(ctx)
if err != nil {
return nil, err
}
defer c.Close()
reminder, err := ptypes.Timestamp(req.ToDo.Reminder)
if err != nil {
return nil, status.Error(codes.InvalidArgument, "reminder field has invalid format-> "+err.Error())
}
// 插入ToDo物體資料
res, err := c.ExecContext(ctx, "INSERT INTO ToDo(`Title`, `Description`, `Reminder`) values (?, ?, ?)",
req.ToDo.Title, req.ToDo.Description, reminder)
if err != nil {
return nil, status.Error(codes.Unknown, "failed to insert into ToDo-> "+err.Error())
}
// 獲取建立ToDo的ID
id, err := res.LastInsertId()
if err != nil {
return nil, status.Error(codes.Unknown, "failed to retrieve id for created ToDo-> "+err.Error())
}
return &v1.CreateResponse;{
Api: apiVersion,
Id: id,
}, nil
}
// 讀取todo任務
func (s *toDoServiceServer) Read(ctx context.Context, req *v1.ReadRequest) (*v1.ReadResponse, error) {
// 檢查伺服器是否支援客戶端請求的API版本
if err := s.checkAPI(req.Api); err != nil {
return nil, err
}
// 從池中獲取sql連線
c, err := s.connect(ctx)
if err != nil {
return nil, err
}
defer c.Close()
// 按照ID查詢ToDo
// 譯者註:實際成功查詢出來的話應該只有一條記錄,因為ID為資料庫的主鍵
rows, err := c.QueryContext(ctx, "SELECT `ID`, `Title`, `Description`, `Reminder` FROM ToDo WHERE `ID`=?", req.Id)
if err != nil {
return nil, status.Error(codes.Unknown, "failed to select from ToDo-> "+err.Error())
}
defer rows.Close()
if !rows.Next() {
if err := rows.Err(); err != nil {
return nil, status.Error(codes.Unknown, "failed to retrieve data from ToDo-> "+err.Error())
}
return nil, status.Error(codes.NotFound, fmt.Sprintf("ToDo with ID='%d' is not found", req.Id))
}
// 獲取ToDo資料
var td v1.ToDo
var reminder time.Time
if err := rows.Scan(&td.Id;, &td.Title;, &td.Description;, &reminder;); err != nil {
return nil, status.Error(codes.Unknown, "failed to retrieve field values from ToDo row-> "+err.Error())
}
td.Reminder, err = ptypes.TimestampProto(reminder)
if err != nil {
return nil, status.Error(codes.Unknown, "reminder field has invalid format-> "+err.Error())
}
// 譯者註:ID為資料庫主鍵
if rows.Next() {
return nil, status.Error(codes.Unknown, fmt.Sprintf("found multiple ToDo rows with ID='%d'", req.Id))
}
return &v1.ReadResponse;{
Api: apiVersion,
ToDo: &td;,
}, nil
}
// 更新ToDo任務
func (s *toDoServiceServer) Update(ctx context.Context, req *v1.UpdateRequest) (*v1.UpdateResponse, error) {
// 檢查伺服器是否支援客戶端請求的API版本
if err := s.checkAPI(req.Api); err != nil {
return nil, err
}
// 從池中獲取sql連線
c, err := s.connect(ctx)
if err != nil {
return nil, err
}
defer c.Close()
reminder, err := ptypes.Timestamp(req.ToDo.Reminder)
if err != nil {
return nil, status.Error(codes.InvalidArgument, "reminder field has invalid format-> "+err.Error())
}
// 更新ToDo
res, err := c.ExecContext(ctx, "UPDATE ToDo SET `Title`=?, `Description`=?, `Reminder`=? WHERE `ID`=?",
req.ToDo.Title, req.ToDo.Description, reminder, req.ToDo.Id)
if err != nil {
return nil, status.Error(codes.InvalidArgument, "reminder field has invalid format-> "+err.Error())
}
rows, err := res.RowsAffected()
if err != nil {
return nil, status.Error(codes.Unknown, "failed to retrieve rows affected value-> "+err.Error())
}
if rows == 0 {
return nil, status.Error(codes.NotFound, fmt.Sprintf("ToDo with ID='%d' is not found", req.ToDo.Id))
}
return &v1.UpdateResponse;{
Api: apiVersion,
Updated: rows,
}, nil
}
// 刪除ToDo任務
func (s *toDoServiceServer) Delete(ctx context.Context, req *v1.DeleteRequest) (*v1.DeleteResponse, error) {
// 檢查伺服器是否支援客戶端請求的API版本
if err := s.checkAPI(req.Api); err != nil {
return nil, err
}
// 從池中獲取sql連線
c, err := s.connect(ctx)
if err != nil {
return nil, err
}
defer c.Close()
// 刪除ToDo
res, err := c.ExecContext(ctx, "DELETE FROM ToDo WHERE `ID`=?", req.Id)
if err != nil {
return nil, status.Error(codes.Unknown, "failed to delete ToDo-> "+err.Error())
}
rows, err := res.RowsAffected()
if err != nil {
return nil, status.Error(codes.Unknown, "failed to retrieve rows affected value-> "+err.Error())
}
if rows == 0 {
return nil, status.Error(codes.NotFound, fmt.Sprintf("ToDo with ID='%d' is not found", req.Id))
}
return &v1.DeleteResponse;{
Api: apiVersion,
Deleted: rows,
}, nil
}
// 讀取所有待辦事項
func (s *toDoServiceServer) ReadAll(ctx context.Context, req *v1.ReadAllRequest) (*v1.ReadAllResponse, error) {
// 檢查伺服器是否支援客戶端請求的API版本
if err := s.checkAPI(req.Api); err != nil {
return nil, err
}
// 從池中獲取sql連線
c, err := s.connect(ctx)
if err != nil {
return nil, err
}
defer c.Close()
// 獲取ToDo串列
rows, err := c.QueryContext(ctx, "SELECT `ID`, `Title`, `Description`, `Reminder` FROM ToDo")
if err != nil {
return nil, status.Error(codes.Unknown, "failed to select from ToDo-> "+err.Error())
}
defer rows.Close()
var reminder time.Time
list := []*v1.ToDo{}
for rows.Next() {
td := new(v1.ToDo)
if err := rows.Scan(&td.Id;, &td.Title;, &td.Description;, &reminder;); err != nil {
return nil, status.Error(codes.Unknown, "failed to retrieve field values from ToDo row-> "+err.Error())
}
td.Reminder, err = ptypes.TimestampProto(reminder)
if err != nil {
return nil, status.Error(codes.Unknown, "reminder field has invalid format-> "+err.Error())
}
list = append(list, td)
}
if err := rows.Err(); err != nil {
return nil, status.Error(codes.Unknown, "failed to retrieve data from ToDo-> "+err.Error())
}
return &v1.ReadAllResponse;{
Api: apiVersion,
ToDos: list,
}, nil
}
Step3:編寫API實現測試
我們正在開發什麼並不重要,我們必須編寫測試。這是必須遵守的規定。
有一個很棒的模擬庫來測試SQL資料庫互動:https://github.com/DATA-DOG/go-sqlmock
我用它來為ToDo服務建立測試。將此檔案放入“pkg / service / v1”檔案夾。
Step4:建立gRPC伺服器
使用以下內容建立檔案“pkg / protocol / grpc / server.go”:
package grpc
import (
"context"
"github.com/fengberlin/go-grpc-http-rest-microservice-tutorial/pkg/api/v1"
"google.golang.org/grpc"
"log"
"net"
"os"
"os/signal"
)
// RunServer執行gRPC服務以釋出ToDo服務
func RunServer(ctx context.Context, v1API v1.ToDoServiceServer, port string) error {
listen, err := net.Listen("tcp", ":"+port)
if err != nil {
return err
}
// 註冊服務
server := grpc.NewServer()
v1.RegisterToDoServiceServer(server, v1API)
// 優雅地關閉
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for range c {
// 訊號是CTRL+C
log.Println("shutting down gRPC server...")
server.GracefulStop()
}
}()
// 啟動gRPC伺服器
log.Println("starting gRPC server...")
// 原作者的啟動gRPC伺服器是這樣子的,但我覺得不太好,所以我改為我的方式去啟動
// return server.Serve(listen)
if err := server.Serve(listen); err != nil {
log.Fatal("starting gRPC server failed...")
return err
}
return nil
}
RunServer函式註冊ToDo服務並啟動gRPC伺服器。
你必須在真實生產環境中為gRPC伺服器配置TLS。請參閱示例如何執行此操作。
接下來建立“pkg / cmd / server.go”檔案,其中包含以下內容:
package server
import (
"context"
"database/sql"
"flag"
"fmt"
"github.com/fengberlin/go-grpc-http-rest-microservice-tutorial/pkg/protocol/grpc"
"github.com/fengberlin/go-grpc-http-rest-microservice-tutorial/pkg/service/v1"
// mysql驅動
_ "github.com/go-sql-driver/mysql"
)
// Config是Server的配置
type Config struct {
// gRPC伺服器啟動引數部分
// GRPCPort是gRPC伺服器監聽的TCP埠
GRPCPort string
// 資料庫資料儲存引數部分
// DatestoreDBHost是資料庫的地址
DatastoreDBHost string
// DatastoreDBUser是用於連線資料庫的使用者名稱
DatastoreDBUser string
// DatastoreDBPassword是用於連線資料庫的密碼
DatastoreDBPassword string
// DatastoreDBSchema是資料庫的名稱
DatastoreDBSchema string
}
// RunServer執行gRPC伺服器和HTTP閘道器
func RunServer() error {
ctx := context.Background()
// 獲取配置
var cfg Config
flag.StringVar(&cfg.GRPCPort;, "grpc-port", "", "gRPC port to bind")
flag.StringVar(&cfg.DatastoreDBHost;, "db-host", "", "Database host")
flag.StringVar(&cfg.DatastoreDBUser;, "db-user", "", "Database user")
flag.StringVar(&cfg.DatastoreDBPassword;, "db-password", "", "Database password")
flag.StringVar(&cfg.DatastoreDBSchema;, "db-schema", "", "Database schema")
flag.Parse()
if len(cfg.GRPCPort) == 0 {
return fmt.Errorf("invalid TCP port for gRPC server: '%s'", cfg.GRPCPort)
}
// 新增MySQL驅動程式特定引數來解析 date/time
// 為另一個資料庫刪除它
param := "parseTime=true"
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?%s", cfg.DatastoreDBUser,
cfg.DatastoreDBPassword, cfg.DatastoreDBHost, cfg.DatastoreDBSchema, param)
db, err := sql.Open("mysql", dsn)
if err != nil {
return fmt.Errorf("failed to open database: %v", err)
}
defer db.Close()
v1API := v1.NewToDoServiceServer(db)
return grpc.RunServer(ctx, v1API, cfg.GRPCPort)
}
此RunServer函式從命令列讀取啟動引數,建立SQL資料庫連線池,建立ToDo服務實體並呼叫gRPC伺服器的前一個RunServer函式。
最後是使用以下內容建立“cmd / server / main.go”檔案:
package main
import (
"fmt"
"github.com/fengberlin/go-grpc-http-rest-microservice-tutorial/pkg/cmd"
"os"
)
func main() {
if err := cmd.RunServer(); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}
Step5:建立gRPC客戶端
使用以下內容建立“cmd / client-grpc / main.go”檔案:
package main
import (
"context"
"flag"
"github.com/fengberlin/go-grpc-http-rest-microservice-tutorial/pkg/api/v1"
"github.com/golang/protobuf/ptypes"
"google.golang.org/grpc"
"log"
"time"
)
const (
// apiVersion是由伺服器提供的API版本
apiVersion = "v1"
)
func main() {
// 獲取配置
address := flag.String("server", "", "gRPC server in format host:port")
flag.Parse()
// 建立與伺服器的連線
conn, err := grpc.Dial(*address, grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := v1.NewToDoServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
t := time.Now().In(time.UTC)
reminder, _ := ptypes.TimestampProto(t)
pfx := t.Format(time.RFC3339Nano)
// 呼叫Create函式
req1 := v1.CreateRequest{
Api:apiVersion,
ToDo:&v1.ToDo;{
Title:"title (" + pfx + ")",
Description:"description (" + pfx + ")",
Reminder:reminder,
},
}
res1, err := c.Create(ctx, &req1;)
if err != nil {
log.Fatalf("Create failed: %v", err)
}
log.Printf("Create result: \n\n", res1)
id := res1.Id
// Read
req2 := v1.ReadRequest{
Api:apiVersion,
Id:id,
}
res2, err := c.Read(ctx, &req2;)
if err != nil {
log.Fatalf("Read failed: %v", err)
}
log.Printf("Read result: \n\n", res2)
// Update
req3 := v1.UpdateRequest{
Api: apiVersion,
ToDo: &v1.ToDo;{
Id: res2.ToDo.Id,
Title: res2.ToDo.Title,
Description: res2.ToDo.Description + " + updated",
Reminder: res2.ToDo.Reminder,
},
}
res3, err := c.Update(ctx, &req3;)
if err != nil {
log.Fatalf("Update failed: %v", err)
}
log.Printf("Update result: \n\n", res3)
// Call ReadAll
req4 := v1.ReadAllRequest{
Api: apiVersion,
}
res4, err := c.ReadAll(ctx, &req4;)
if err != nil {
log.Fatalf("ReadAll failed: %v", err)
}
log.Printf("ReadAll result: \n\n", res4)
// Delete
req5 := v1.DeleteRequest{
Api: apiVersion,
Id: id,
}
res5, err := c.Delete(ctx, &req5;)
if err != nil {
log.Fatalf("Delete failed: %v", err)
}
log.Printf("Delete result: \n\n", res5)
}
Step6:執行gRPC服務端和客戶端
最後一步是確保gRPC伺服器正常工作。啟動終端以構建和執行gRPC伺服器(根據SQL資料庫伺服器替換引數,schema即是你資料庫的名稱):
cd cmd/server
go run main.go -grpc-port=9090 -db-host=:3306 -db-user= -db-password= -db-schema=
如果我們看到:
2019/01/28 22:14:03 starting gRPC server...
這意味著伺服器已啟動。
開啟另一個終端來構建和執行gRPC客戶端:
cd cmd/client-grpc
go run main.go -server=localhost:9090
如果我們看到這樣的東西:
2019/01/28 22:15:16 Create result: "v1" id:1 >
2019/01/28 22:15:16 Read result: "v1" toDo:<1 class="hljs-string" style="font-size: inherit;line-height: inherit;color: rgb(238, 220, 112);word-wrap: inherit !important;word-break: inherit !important;">"title (2019-01-28T14:15:16.411604041Z)" description:"description (2019-01-28T14:15:16.411604041Z)" reminder:<1548684916> > >
2019/01/28 22:15:16 Update result: "v1" updated:1 >
2019/01/28 22:15:16 ReadAll result: "v1" toDos:<1 class="hljs-string" style="font-size: inherit;line-height: inherit;color: rgb(238, 220, 112);word-wrap: inherit !important;word-break: inherit !important;">"title (2019-01-28T14:15:16.411604041Z)" description:"description (2019-01-28T14:15:16.411604041Z) + updated" reminder:<1548684916> > >
2019/01/28 22:15:16 Delete result: "v1" deleted:1 >
</1548684916></1></1548684916></1>
一切執行正常。
第一部分總結
這就是第1部分的全部內容。我們開發了gRPC服務和客戶端。
第2部分專門介紹如何將HTTP / REST端點新增到我們今天開發的gRPC服務中。
謝謝!
via: https://medium.com/@amsokol.com/tutorial-how-to-develop-go-grpc-microservice-with-http-rest-endpoint-middleware-kubernetes-daebb36a97e9
作者: Aleksandr Sokolovskii
譯者: Berlin
朋友會在“發現-看一看”看到你“在看”的內容