有很多文章介绍了如何使用几个优秀的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
第五届 Gopher China 大会更多动态:
【重要通知】@所有人,大会报名倒计时11天!欲报从速,错过再等一年!
Gopher China 2019 讲师专访-bilibili架构师毛剑
Gopher China 2019 讲师专访-TutorABC研发总监董海冰
探探Gopher China 2019大会全面启动
最后两周粉丝福利:
福利优惠码:GopherChina
Gopher China 2019大会企业团购通道即将关闭
详情请加微信号:13458572960(玉璧)
戳下方“阅读原文”即可报名本次 Gopher China 大会!
GO 中国征稿啦!
自“Go中国 ” 公众号上线以来,因为扎实的干货(害羞)、前沿的解读(娇羞)、满满的福利一直深受 Gopher 们的喜爱,为了给大家带来更具实力的干货以及 Go 语项目开发经验,我们将开始对外征稿!
现在我们开始对外征稿啦!如果你有优秀的 Go 语言技术文章想要分享,热点的行业资讯需要报道等,欢迎联系在菜单栏回复“投稿”“合作”联系我们的小编进行投稿。