Golang中如何选择REST和gRPC进行API开发

Golang中如何选择REST和gRPC进行API开发 我正在尝试使用Go和gRPC构建REST API。这似乎不是构建REST API最简单的方法,但希望是最快的……

这方面的基础内容几乎到处都有涉及,但我还没找到任何清晰的路线图或简单教程。目前我认为可能包含四个部分:

  1. 网页(基于文本的AJAX-json?)
  2. 桥接代理(Envoy或Nginx)- 文本到二进制的转换?
  3. 带端点的Go组件(go-web还是grpc-web?)
  4. PostgreSQL数据库

举个例子:在不使用Docker的情况下安装Envoy需要高超的技术能力。需要依赖Javascript、Python、Bazel等工具……

非常欢迎提供任何实现此方案的技巧、线索或路线图。

这真的可行吗?

先谢过了


更多关于Golang中如何选择REST和gRPC进行API开发的实战教程也可以访问 https://www.itying.com/category-94-b0.html

20 回复

更多关于Golang中如何选择REST和gRPC进行API开发的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


iegomez: 它内部会通过网关向gRPC服务器发起调用

那么网关不是服务器吗?那它是什么?

iegomez:

这些内容与我向你说明的问题完全无关。

感谢您的耐心解答。但对我来说,这关乎于理解全貌。您的解释让事情清晰多了。

嗯,但是你的"原始 Go Web 服务器 http://94.237.25.207:8080/"与 gRPC + HTTP 网关有什么关系?据我所知,你可能还运行着 10、20 或 30 个与我所讨论内容完全无关的其他 Go Web 服务器。

我在你的回答中看到了一些误解:Web服务器是客户(什么的客户?)?REST端与Web服务器是分开的?Go作为前端(我猜是使用gopherjs或wasm)?JSON解析速度取决于不相关的协议/技术?我对此不太理解。

iegomez iegomez:

现在,这绝不是关于如何实现这一点的指南

谢谢!但是我的SQL查询应该放在哪里?

你可以查看 https://github.com/brocaar/lora-app-server 并检查其结构。前端是一个 React 应用程序,通过 swagger 客户端与 REST API 进行通信,而该客户端又是使用 https://github.com/grpc-ecosystem/grpc-gateway 从 gRPC 生成的。

iegomez:

那么是2个服务器,而不是3个。

我也在使用Go语言作为网页服务器。如果我的网页在一个"网页服务器"上,而"grpc-gateway将创建一个HTTP服务器"。

我数出来是3个服务器:

  1. 原始的Go网页服务器 http://94.237.25.207:8080/
  2. 网关服务器(“grpc-gateway将创建一个HTTP服务器”)
  3. gRPC服务器

还是说网关服务器会取代我原来的服务器?

iegomez 提到: 你可以查看 https://github.com/brocaar/lora-app-server 并检查其结构。

这个拼图中还有很多我不理解的部分。每个组件具体负责什么功能,以及它们的执行顺序是怎样的。

我之前使用过传统的 REST API,它基本上包含两个部分:

浏览器

1. GET users

Go 服务器

2. 连接到 Postgresql
3. 监听并服务(启动服务器)
4. 路由到端点
5. 获取 SQL 数据
6. 将结果解析为 JSON 并返回给浏览器

如何将这些转换为 gRPC?我需要使用哪些组件?执行顺序是怎样的?

提前感谢任何能让这个问题更清晰的线索或链接。

iegomez:

我在你的回答中看到了一些误解:Web服务器是客户(什么的客户)?

这是一个比喻。顾客 > 服务员 > 厨师。这是向完全的新手(我)解释时非常常用的说法。这个比喻指的是:提出问题(下单)> 翻译成二进制 > 从数据库获取。或者 Web > 网关 > gRPC 服务器。

REST 端与 Web 服务器是分开的吗?

我无法理解你的问题……

Go 作为前端(我猜是使用 gopherjs 或 wasm)?

不。目标是使用普通的原生 JavaScript。

JSON 解析速度取决于不相关的协议/技术?我不太理解这一点。

嗯,你是在和一个新手说话。我可能还不太会提出有水平的问题……

好的,现在我明白了,我原以为您说的是一个独立的应用程序。如果您查看我之前展示了一堆示例文件的帖子,其中有这样一个部分用于为React前端提供静态文件服务:

// setup static file server
	r.PathPrefix("/").Handler(http.FileServer(&assetfs.AssetFS{
		Asset:     static.Asset,
		AssetDir:  static.AssetDir,
		AssetInfo: static.AssetInfo,
		Prefix:    "",
	}))

实际上并不一定需要React或其他复杂的前端框架,Go的HTML模板完全够用。您只需要提供模板服务,然后通过JavaScript从这些模板向API端点发起ajax调用。因此,HTTP服务器将扮演两种角色:提供服务器端渲染的HTML,同时为ajax调用暴露REST API。所以没错,这些调用都是指向同一个应用程序的。

这是 grpc-gateway 的第一段描述:

grpc-gateway 是 Google 协议缓冲区编译器 protoc 的一个插件。它读取 protobuf 服务定义,并生成一个反向代理服务器,该服务器将 RESTful JSON API 转换为 gRPC。此服务器根据服务定义中的 google.api.http 注解生成。

简而言之,grpc-gateway 将创建一个 HTTP 服务器,当收到任何请求时,该服务器将在底层调用 gRPC(因此得名 gateway)。所以是两个服务器,而不是三个。

如果之前表达有误,我的意思是说,您正在运行的任何其他面向网络的应用程序,无论是用Go、Ruby、Python等编写的,都完全独立于您正在尝试构建的这个特定的面向网络的应用程序,该应用程序恰好在其内部提供gRPC和HTTP服务,或者正如我们所说,实现了两个服务器:一个gRPC服务器,一个HTTP服务器。

回到核心问题,我认为一个好的总结是:

  1. 检查Go中的项目布局策略,更具体地说是Web项目,以便安排您的内部包(例如存储)。
  2. 问问自己为什么首先要使用gRPC。典型的Web应用程序有什么问题?您考虑使用gRPC的需求或要求是什么?您只是想学习gRPC吗?那为什么还需要HTTP API转换呢?
  3. 现在阅读并学习协议缓冲区和gRPC,以便定义和理解proto服务变得自然而然。
  4. 查看grpc-gateway仓库及其文档,以便熟悉它的功能和工作原理。
  5. 编写一些HTML模板或使用一些调用您API的前端库/框架。
  6. 休息。

Sibert:

这是一个比喻。顾客 > 服务员 > 厨师。这是向完全的新手(我)解释时非常常见的说法。它比喻的是:提出问题(下单)> 翻译成二进制 > 从数据库获取数据。或者 Web > 网关 > gRPC 服务器。

我明白了,只是没注意到这两个回答是相关的。无论如何,我仍然认为你有些困惑。其他网络应用程序并不属于整体架构的一部分,因为它们与我们讨论的应用没有任何关系:它们不是"顾客",也不会发出任何请求。请求来自网络客户端,由应用程序中的 HTTP 服务器处理,然后进行 gRPC 调用,依此类推。中间没有中介或其他组件参与。这并不是说不能有(例如,你可能使用了像 nginx 这样的反向代理),但并不是必需的。

作为"新手"并没有问题,我们都曾是新手,只是我对你提出的一些问题感到困惑。例如,你如何在前端运行 Go 并声称它比 Angular 快两倍?在这种情况下,你所说的"前端"是指什么?

再次强调,这只是其中一种实现方式。在这个特定项目中,有一个内部的 api 包和另一个 storage 包,api 实现会调用这些包来创建和消费数据(例如数据验证、执行必要查询等)。项目结构有很多种方式,论坛里也讨论过几次(可以搜索项目布局或类似关键词)。

在本论坛搜索"项目布局 grpc"只返回了1个结果。这个问题 🙂

但我有点只见树木不见森林。所以我尝试从宏观视角来理解。以下是我的理解:

根据我的解读,这需要3个服务器?Web服务器、网关服务器,最后是gRPC服务器。对吗?

浏览器(Web服务器)

1. GET users

网关服务器(负责JSON和二进制格式之间的转换)?

6. 将结果解析为JSON并返回给浏览器

gRPC服务器

2. 连接到Postgresql
3. 监听并服务(启动服务器)
4. 路由到端点
5. 存储SQL和获取SQL数据

我猜想网关和gRPC服务器使用的是相同的结构体?(通过proto之类的工具神秘生成的)

我的理解正确吗?如果有误请指正!

Sibert:

在论坛中搜索"项目布局 grpc"只得到1个结果。这个问题 🙂

我指的是通用的Go项目,不是特指gRPC。你看,无论是gRPC还是HTTP API,都不应该改变你抽象和访问数据层的方式,对吧?在这种情况下,如果你去掉其中任何一个,数据模型的internal/storage/...结构都会保持不变。

Sibert:

按照我的理解,这需要3个服务器?Web服务器、网关服务器,最后是gRPC服务器。对吗?

实际上不是,只有2个服务器:gRPC服务器和HTTP服务器。JSON API只是你传递给HTTP服务器的处理程序,它本身不是一个服务器。

Sibert:

我猜想网关和gRPC服务器都使用相同的结构体?

再次说明,网关只是gRPC和JSON HTTP API之间的转换层:当你在HTTP服务器中收到HTTP请求时,它会通过网关内部调用gRPC服务器,获取其响应,然后发送HTTP响应。所以是的,数据结构是"共享的",因为逻辑实际上只实现了一次。

Sibert:

通过proto某种方式神秘地生成

如果你觉得它很神秘,那么我认为在盲目使用之前,你真的应该阅读关于gRPC协议缓冲区的资料。

抱歉如果表达有误,我的意思是说您正在运行的任何其他面向网络的应用程序,无论是用Go、Ruby、Python等语言编写的,都完全独立于您正在尝试构建的这个特定面向网络的应用程序——它碰巧同时提供gRPC和HTTP服务,或者像我们所说的,实现了两个服务器:一个gRPC服务器,一个HTTP服务器。

要描述披萨订购的流程,您不能忽略顾客环节。这关乎理解整体架构,并非技术问题。

  1. 查阅Go项目的布局策略,特别是Web项目,以便合理组织内部包(例如存储包)。

对于"顾客"部分(Web服务器),我很清楚如何组织模板等。但在"REST"方面,如何存储和检索大量SQL查询仍不明确…

  1. 首先问自己为什么要使用gRPC。传统Web应用有什么问题?您有哪些需求或要求需要考虑?您只是想要学习gRPC吗?那为什么还需要HTTP API转换呢?

这是个很好的问题。我的最终应用将高度依赖与PostgreSQL服务器之间的数据流量。而速度是主要考量因素。我目前还没有足够经验在普通REST服务和gRPC之间做出选择。这两种方式在JSON解析速度上有差异吗?任何建议都将不胜感激。

  1. 现在阅读并学习协议缓冲区和gRPC,这样定义和理解proto服务就会变得很自然。

我认为协议缓冲区能半自动生成Go结构体,使用gRPC能更轻松地更新结构体变更。如果我说错了请指正。

  1. 查看grpc-gateway代码库及其文档,以便熟悉它的功能和工作原理。

我会去了解的…

  1. 编写一些HTML模板或使用某些前端库/框架来调用您的API。

我之前尝试过Angular和Go作为前端。Go的速度至少快两倍。所以在有更深入了解之前,我会坚持使用Go。

iegomez:

我明白了,只是没注意到这两个回答是相关的。无论如何,我仍然认为你有些困惑。

我同意 🙂

其他网络应用程序不属于整体架构的一部分,因为它们与我们讨论的内容无关:它们不是"客户",不会发出任何请求。请求来自网络客户端,由应用程序中的HTTP服务器处理,然后进行gRPC调用等等。

那么网关是集成到网络服务器中的吗?AJAX调用自己的服务器?也就是提供HTML页面的同一个服务器?

作为"新手"并没有问题,我们都是这样开始的,只是我对某些问题感到困惑。

新手不知道如何提出正确的问题。作为专业人士,有时你会认为某些事情是理所当然的。

例如,你如何在前端运行Go,并声称它比Angular快两倍?在这种情况下,你所说的前端是什么意思?

这是我需要询问REST问题的测试前端服务器。基本上提供与Angular相同的内容。速度快两倍。

var tpl *template.Template

func init() {
	tpl = template.Must(template.ParseGlob("public/templates/*.html"))
}

func main() {
	http.HandleFunc("/", index)
	http.Handle("/img/", http.StripPrefix("/img/", http.FileServer(http.Dir("./public/img"))))
	http.Handle("/css/", http.StripPrefix("/css/", http.FileServer(http.Dir("./public/css"))))
	http.Handle("/icn/", http.StripPrefix("/icn/", http.FileServer(http.Dir("./public/icn"))))

	http.ListenAndServe(":8080", nil)
}

func index(w http.ResponseWriter, r *http.Request) {....

https://developers.google.com/speed/pagespeed/insights/?url=http%3A%2F%2F94.237.25.207%3A8080%2F

如果你查看用户API中的代码,会在Get方法中看到这样的内容:

user, err := storage.GetUser(storage.DB(), req.Id)

这只是调用了storage包的GetUser函数,在这个项目中位于lora-app-server/internal/storage/user.go文件,具体实现如下:

package storage

//一些导入和变量声明

//用户结构体
// User 对外部代码表示一个用户
type User struct {
	ID           int64     `db:"id"`
	Username     string    `db:"username"`
	IsAdmin      bool      `db:"is_admin"`
	IsActive     bool      `db:"is_active"`
	SessionTTL   int32     `db:"session_ttl"`
	CreatedAt    time.Time `db:"created_at"`
	UpdatedAt    time.Time `db:"updated_at"`
	PasswordHash string    `db:"password_hash"`
	Email        string    `db:"email"`
	Note         string    `db:"note"`
}

const externalUserFields = "id, username, is_admin, is_active, session_ttl, created_at, updated_at, email, note"
const internalUserFields = "*"

/// GetUser 返回指定id对应的用户
func GetUser(db sqlx.Queryer, id int64) (User, error) {
	var user User
	err := sqlx.Get(db, &user, "select "+externalUserFields+" from \"user\" where id = $1", id)
	if err != nil {
		if err == sql.ErrNoRows {
			return user, ErrDoesNotExist
		}
		return user, errors.Wrap(err, "select error")
	}

	return user, nil
}

再次说明,这只是其中一种实现方式。在这个特定项目中,有一个内部的api包和另一个storage包,api实现会调用storage包来创建和消费数据(即进行数据验证、执行必要查询等)。项目结构有很多种组织方式,论坛里也讨论过几次(可以搜索项目布局或类似关键词)。

在Go语言中同时使用REST和gRPC进行API开发是完全可行的,而且这种架构在现代微服务系统中很常见。让我通过具体示例来说明如何实现。

架构实现方案

1. 定义gRPC服务

首先定义protobuf文件 user_service.proto

syntax = "proto3";

package user;

service UserService {
  rpc GetUser(GetUserRequest) returns (UserResponse);
  rpc CreateUser(CreateUserRequest) returns (UserResponse);
}

message GetUserRequest {
  string user_id = 1;
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
}

message UserResponse {
  string id = 1;
  string name = 2;
  string email = 3;
}

生成Go代码:

protoc --go_out=. --go-grpc_out=. user_service.proto

2. 实现gRPC服务端

package main

import (
	"context"
	"log"
	"net"

	"google.golang.org/grpc"
	pb "path/to/your/proto"
)

type userServer struct {
	pb.UnimplementedUserServiceServer
}

func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.UserResponse, error) {
	// 数据库查询逻辑
	return &pb.UserResponse{
		Id:    req.UserId,
		Name:  "John Doe",
		Email: "john@example.com",
	}, nil
}

func (s *userServer) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.UserResponse, error) {
	// 数据库插入逻辑
	return &pb.UserResponse{
		Id:    "123",
		Name:  req.Name,
		Email: req.Email,
	}, nil
}

func main() {
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	
	grpcServer := grpc.NewServer()
	pb.RegisterUserServiceServer(grpcServer, &userServer{})
	
	log.Println("gRPC server listening on :50051")
	if err := grpcServer.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

3. 实现REST网关

使用grpc-gateway创建REST到gRPC的转换:

package main

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

	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

	pb "path/to/your/proto"
)

func runRESTGateway() {
	ctx := context.Background()
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	mux := runtime.NewServeMux()
	opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
	
	err := pb.RegisterUserServiceHandlerFromEndpoint(ctx, mux, "localhost:50051", opts)
	if err != nil {
		log.Fatalf("failed to register gateway: %v", err)
	}

	log.Println("REST gateway listening on :8080")
	http.ListenAndServe(":8080", mux)
}

func main() {
	go runRESTGateway()
	
	// 同时运行gRPC服务
	runGRPCServer()
}

4. 前端调用示例

前端可以通过REST API调用:

// 获取用户
fetch('http://localhost:8080/v1/users/123')
  .then(response => response.json())
  .then(data => console.log(data));

// 创建用户
fetch('http://localhost:8080/v1/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    name: 'Jane Doe',
    email: 'jane@example.com'
  })
})
.then(response => response.json())
.then(data => console.log(data));

5. 数据库集成

集成PostgreSQL的示例:

import (
	"database/sql"
	_ "github.com/lib/pq"
)

type UserRepository struct {
	db *sql.DB
}

func (r *UserRepository) GetUserByID(ctx context.Context, id string) (*pb.UserResponse, error) {
	var user pb.UserResponse
	err := r.db.QueryRowContext(ctx, 
		"SELECT id, name, email FROM users WHERE id = $1", id).
		Scan(&user.Id, &user.Name, &user.Email)
	if err != nil {
		return nil, err
	}
	return &user, nil
}

部署方案

使用Nginx作为反向代理

http {
    upstream grpc_servers {
        server localhost:50051;
    }
    
    upstream rest_gateway {
        server localhost:8080;
    }
    
    server {
        listen 80;
        
        location /api/ {
            proxy_pass http://rest_gateway/;
        }
        
        location / {
            # 静态文件服务
            root /var/www/html;
        }
    }
    
    server {
        listen 50052 http2;
        
        location / {
            grpc_pass grpc://grpc_servers;
        }
    }
}

这种架构的优势在于:

  • 内部服务间使用高效的gRPC通信
  • 外部客户端使用熟悉的REST API
  • 单一代码库维护两种接口
  • 自动生成API文档和客户端代码

整个方案不需要Docker,可以直接在服务器上部署运行。

回到顶部