Go中实现API的两种方法:REST or gRPC


本文,我们来研究一下在Go中实现HTTP API的两种方法(REST和gRPC)及其几种工具。

REST

有些人不太理解REST,所以他们还在继续使用RPC风格来开发API。这样做的原因是wiki的信息量还不足以支撑用户使用REST的全部功能。因此,今天这个话题就很受欢迎,而且以后也会有越来越多的关于REST的文章出现在博客和各种座谈会上。

Zalando的指南帮助我们理解了什么是REST。该文档与OpenAPI的倡议和设计优先(Design First)方法一起出现。据此,首先我们需要描述API,然后再开始实现业务逻辑。相关文档在这里

Swagger(swagger.io)帮助构建REST API。如果在node.js上编写会相当简单。然而,在Go的情况下要复杂得多。你必须设计快速的解决方案,比如测试,首先要检查endpoint是否有相应的文档或swagger.yml是否有Go的数据结构生成器。

如何在Go中开发一个完整的和受支持的REST API而不需要swagger支持?

其中一些选项如下:
  1. 写swagger.yml
  2. 描述一个模型
  3. 从API一侧和数据库工作层来实现CRUD
  4. 用API测试来覆盖模型
  5. 在此基础上,去统一和制作一个生成器


结果就是,我们得到如下算法:描述模型,编写代码,实现业务逻辑。

作为一个额外的方法,我们还可以从swagger.yml生成模型。

所有带REST API的应用程序都是在常规的3层架构上构建的。

我曾经使用过echo框架创建应用程序。它具有良好的设计和速度。我们使用sqlx作为数据库层。使用这个集合和自编写的代码生成器,我们可以非常快速地构建一个API。然而,我仍然建议使用Django或Flask中的Python来构建API。这个解决方案只有在你不期望高负载时才有效。

gRPC

gRPC作为一款来自Google的工具,不仅仅在Go社区而且在其以外都被积极地推广。默认情况下,gRPC允许你通过HTTP/2创建良好的内部API。

通常来说,gRPC的功能是相当好的,但是它最重要的功能在于默认的代码生成和设计优先理念的推广。

当使用swagger时,很有可能不做支持文档,因为这通常是一个额外的困扰。然而,当你使用gRPC时就不会有这种情况,这个改进的工具简化了开发人员的工作,因为它不可能在没有原型文件的情况下编写代码,更不可能不与开发团队共享。

使用gRPC的一个理想场景是,在微服务间进行的连接,以及移动应用与服务器的通信。

gRPC主要是一个RPC框架,它引用UpdateBalance、GetClient方法。

REST是关于诸如GET/users、POST/users等资源的。

在gRPC中,可以创建许多方法,直到达到Go接口的限制(512个方法/1个接口)。

由于这篇文章主要是关于gRPC的,让我们从grpc-go文档中选择一个简单的“hello, world”示例,并使用插件来改进它。我们将在gRPC服务中添加数据验证和REST API并生成swagger.json及其文档:
syntax = "proto3";

package helloworld;

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;


项目结构如下:
  1. Pb——用来存储原型文件和已生成代码的包
  2. main.go——用来配置和运行我们的应用程序的一个service
  3. server——用来实现应用程序逻辑的包
  4. Makefile——使实例自动化的文件


为了生成代码,你应该运行:
protoc --go_out=plugins=grpc:.

起初,Makefile中描述的一些命令就足以使例程自动化:
TARGET=helloworld

all: clean build

clean:
rm -rf $(TARGET)

build:
go build -o $(TARGET) main.go

proto:
protoc --go_out=plugins=grpc:. pb/hello.proto

我们需要先Makefile从而可以输入make proto,这样就可以从原型文件生成代码。此外,这一部分还会被扩展,命令将变得更长,代码数量也随之增加。

如果我们生成代码实现服务器的主要接口,生成一个API将得到以下代码:
server/server.go
package server

import (
"context"
"fmt"
"net"

pb "github.com/maddevsio/grpc-rest-api-example/pb"
"google.golang.org/grpc"
)

// Greeter ...
type Greeter struct {
}

// New creates new server greeter
func New() *Greeter {
return &Greeter{}
}

// Start starts server
func (g *Greeter) Start() error {
lis, err := net.Listen("tcp", "localhost:8080")
if err != nil {
    return err
}
grpcServer := grpc.NewServer()
pb.RegisterGreeterServer(grpcServer, g)
grpcServer.Serve(lis)
return nil
}

// SayHello says hello
func (g *Greeter) SayHello(ctx context.Context, r *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{
    Message: fmt.Sprintf("Hello, %s!", r.Name),
}, nil


从main.go按如下方式运行:
package main

import "github.com/maddevsio/grpc-rest-api-example/server"

func main() {
g := server.New()
g.Start()


gRPC-gateway

最近,在一个项目中,我们决定使用gRPC进行服务之间的通信。同时,我们还需要为外部客户继续使用良好的REST。

我们都知道这个工具,我们甚至用这个插件做了一个项目。在这个项目里,会有同时支持几项做同样事情的服务,然而这个前景并不被看好。但在这种情况下,gRPC-gateway却依然非常有用,因为我们将在一个原型文件中同时包含gRPC和REST描述。

根据README的说明,我们先安装一下,然后再把我们的原型文件转换成以下形式:
syntax = "proto3";

import "google/api/annotations.proto";

package helloworld;

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {
    option(google.api.http) = {
        get: "/api/hello/{name}",
    };
}
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;


此外,我们应该改变命令去生成代码,继而从原型文件到以下形式:
TARGET=helloworld

all: clean build

clean:
rm -rf $(TARGET)

build:
go build -o $(TARGET) main.go

proto:
protoc -I/usr/local/include -I. \
    -I${GOPATH}/src \
    -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
    --go_out=plugins=grpc:. \
    pb/hello.proto
protoc -I/usr/local/include -I. \
    -I${GOPATH}/src \
    -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
    --grpc-gateway_out=logtostderr=true:. \
    pb/hello.proto
protoc -I/usr/local/include -I. \
    -I${GOPATH}/src \
    -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
    --swagger_out=logtostderr=true:. \
    pb/hello.proto

现在,proto命令将为逻辑的实施生成rest网关、swagger和接口。

那么,现在有一个小问题——我们的服务器将监听两个TCP套接字。一个是确保gRPC机制的运行,第二个是确保REST的。此外,网关将作为中介发布,将JSON HTTP REST表述转换为原型文件上的gRPC。

让我们修改一下代码来运行服务器,代码如下:
package server

import (
"context"
"fmt"
"log"
"net"
"net/http"
"sync"

"github.com/grpc-ecosystem/grpc-gateway/runtime"
pb "github.com/maddevsio/grpc-rest-api-example/pb"
"google.golang.org/grpc"
)

// Greeter ...
type Greeter struct {
wg sync.WaitGroup
}

// New creates new server greeter
func New() *Greeter {
return &Greeter{}
}

// Start starts server
func (g *Greeter) Start() {
g.wg.Add(1)
go func() {
    log.Fatal(g.startGRPC())
    g.wg.Done()
}()
g.wg.Add(1)
go func() {
    log.Fatal(g.startREST())
    g.wg.Done()
}()
}
func (g *Greeter) startGRPC() error {
lis, err := net.Listen("tcp", "localhost:8080")
if err != nil {
    return err
}
grpcServer := grpc.NewServer()
pb.RegisterGreeterServer(grpcServer, g)
grpcServer.Serve(lis)
return nil
}
func (g *Greeter) startREST() error {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()

mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithInsecure()}
err := pb.RegisterGreeterHandlerFromEndpoint(ctx, mux, ":8080", opts)
if err != nil {
    return err
}

return http.ListenAndServe(":8090", mux)
}

// SayHello says hello
func (g *Greeter) SayHello(ctx context.Context, r *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{
    Message: fmt.Sprintf("Hello, %s!", r.Name),
}, nil


现在运行我们的服务器和REST请求:
$ curl -XGET http://localhost:8090/api/hello/Mike
{"message":"Hello, Mike!"} 

至此,我们在gRPC之上获得REST API。但是在本例中,并没有足够的验证设置。关于验证,我们会使用https://github.com/lyft/protoc-gen-validate。按照文档中的示例,我们将把原型文件转换成如下这种形式:
syntax = "proto3";

import "google/api/annotations.proto";
import "validate/validate.proto";

package helloworld;

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {
    option(google.api.http) = {
        get: "/api/hello/{name}",
    };
}
}

message HelloRequest {
string name = 1 [(validate.rules).string.len = 3];
}

message HelloReply {
string message = 1;


让我们添加另一个命令来生成代码:
TARGET=helloworld

all: clean build

clean:
rm -rf $(TARGET)

build:
go build -o $(TARGET) main.go

proto:
protoc -I/usr/local/include -I. \
    -I${GOPATH}/src \
    -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
    -I${GOPATH}/src/github.com/lyft/protoc-gen-validate \
    --go_out=plugins=grpc:. \
    pb/hello.proto
protoc -I/usr/local/include -I. \
    -I${GOPATH}/src \
    -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
    -I${GOPATH}/src/github.com/lyft/protoc-gen-validate \
    --grpc-gateway_out=logtostderr=true:. \
    pb/hello.proto
protoc -I/usr/local/include -I. \
    -I${GOPATH}/src \
    -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
    -I${GOPATH}/src/github.com/lyft/protoc-gen-validate \
    --swagger_out=logtostderr=true:. \
    pb/hello.proto
protoc -I/usr/local/include -I. \
    -I${GOPATH}/src \
    -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
    -I${GOPATH}/src/github.com/lyft/protoc-gen-validate \
    --validate_out="lang=go:." \
    pb/hello.proto

在代码重新生成之后,我们可以看到现在验证可以生效了。
$ curl -XGET http://localhost:8090/api/hello/
{"error":"invalid HelloRequest.Name: value length must be 3 runes","message":"invalid HelloRequest.Name: value length must be 3 runes","code":2} 

结论

gRPC生态系统对Go中的Web服务非常友好。也正是基于这一点,我们可以通过生成大部分重复的代码来加快开发的过程。在这里可以看到项目的完整代码。

如果你写了REST API,但不知道如何描述你的API,那么就选择gRPC吧,在Go中还可以节省更多开发Web服务的时间!

原文链接:Go. REST or gRPC(翻译:伊海峰)

0 个评论

要回复文章请先登录注册