[教程,Part 1]如何使用HTTP/REST端点,中间件,Kubernetes等开发Go gRPC微服务

640?wx_fmt=png

有很多文章介绍了如何使用几个优秀的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/<you-github-name>/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()
            <-ctx.Done()
        }
    }()

    // 启动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: <%+v>\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: <%+v>\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: <%+v>\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: <%+v>\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: <%+v>\n\n", res5)
}

Step6:运行gRPC服务端和客户端

最后一步是确保gRPC服务器正常工作。启动终端以构建和运行gRPC服务器(根据SQL数据库服务器替换参数,schema即是你数据库的名称):

cd cmd/server
go run main.go -grpc-port=9090 -db-host=<HOST>:3306 -db-user=<USER> -db-password=<PASSWORD> -db-schema=<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: <api:"v1" id:1 >

2019/01/28 22:15:16 Read result: <api:"v1" toDo:<id:1 title:"title (2019-01-28T14:15:16.411604041Z)" description:"description (2019-01-28T14:15:16.411604041Z)" reminder:<seconds:1548684916 > > >

2019/01/28 22:15:16 Update result: <api:"v1" updated:1 >

2019/01/28 22:15:16 ReadAll result: <api:"v1" toDos:<id:1 title:"title (2019-01-28T14:15:16.411604041Z)" description:"description (2019-01-28T14:15:16.411604041Z) + updated" reminder:<seconds:1548684916 > > >

2019/01/28 22:15:16 Delete result: <api:"v1" deleted: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



640?wx_fmt=png


第五届 Gopher China 大会更多动态:


【重要通知】@所有人,大会报名倒计时11天!欲报从速,错过再等一年!


Gopher China 2019 讲师专访-bilibili架构师毛剑


Gopher China 2019 讲师专访-TutorABC研发总监董海冰

探探Gopher China 2019大会全面启动


最后两周粉丝福利:


福利优惠码:GopherChina


Gopher China  2019大会企业团购通道即将关闭

详情请加微信号:13458572960(玉璧)


戳下方阅读原文即可报名本次 Gopher China 大会!


640?wx_fmt=png

GO 中国征稿啦!


自“Go中国  ” 公众号上线以来,因为扎实的干货(害羞)、前沿的解读(娇羞)、满满的福利一直深受 Gopher 们的喜爱,为了给大家带来更具实力的干货以及 Go 语项目开发经验,我们将开始对外征稿!


现在我们开始对外征稿啦!如果你有优秀的 Go 语言技术文章想要分享,热点的行业资讯需要报道等,欢迎联系在菜单栏回复“投稿”“合作”联系我们的小编进行投稿。‍

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值