
有很多文章介紹瞭如何使用幾個優秀的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
知識星球
朋友會在“發現-看一看”看到你“在看”的內容