歡迎光臨
每天分享高質量文章

[教程,Part 1]如何使用HTTP/REST端點,中介軟體,Kubernetes等開發Go gRPC微服務

有很多文章介紹瞭如何使用幾個優秀的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

 

已同步到看一看
贊(0)

分享創造快樂