何时使用单体 RESTful 服务

对于很多初创公司来说,业务的早期我们更应该关注于业务价值的交付,而单体服务具有架构简单,部署简单,开发成本低等优点,可以帮助我们快速实现产品需求。我们在使用单体服务快速交付业务价值的同时,也需要为业务的发展预留可能性,所以我们一般会在单体服务中清晰的拆分不同的业务模块。

商城单体 RESTful 服务

我们以商城为例来构建单体服务,商城服务一般来说相对复杂,会由多个模块组成,比较重要的模块包括账号模块、商品模块和订单模块等,每个模块会有自己独立的业务逻辑,同时每个模块间也会相互依赖,比如订单模块和商品模块都会依赖账号模块,在单体应用中这种依赖关系一般是通过模块间方法调用来完成。一般单体服务会共享存储资源,比如 MySQLRedis 等。

单体服务的整体架构比较简单,这也是单体服务的优点,客户请求通过 DNS 解析后通过 Nginx 转发到商城的后端服务,商城服务部署在 ECS 云主机上,为了实现更大的吞吐和高可用一般会部署多个副本,这样一个简单的平民架构如果优化好的话也是可以承载较高的吞吐的。

商城服务内部多个模块间存在依赖关系,比如请求订单详情接口 /order/detail,通过路由转发到订单模块,订单模块会依赖账号模块和商品模块组成完整的订单详情内容返回给用户,在单体服务中多个模块一般会共享数据库和缓存。

单体服务实现

接下来介绍如何基于 go-zero 来快速实现商城单体服务。使用过 go-zero 的同学都知道,我们提供了一个 API 格式的文件来描述 Restful API,然后可以通过 goctl 一键生成对应的代码,我们只需要在 logic 文件里填写对应的业务逻辑即可。商城服务包含多个模块,为了模块间相互独立,所以不同模块由单独的 API 定义,但是所有的 API 的定义都是在同一个 service (mall-api) 下。

api 目录下分别创建 user.api, order.api, product.apimall.api,其中 mall.api 为聚合的 api 文件,通过 import 导入,文件列表如下:

api
|-- mall.api
|-- order.api
|-- product.api
|-- user.api

Mall API 定义

mall.api 的定义如下,其中 syntax = “v1” 表示这是 zero-apiv1 语法

syntax = "v1"

import "user.api"
import "order.api"
import "product.api"

账号模块 API 定义

  • 查看用户详情
  • 获取用户所有订单
syntax = "v1"

type (
UserRequest {
ID int64 `path:"id"`
} UserReply {
ID int64 `json:"id"`
Name string `json:"name"`
Balance float64 `json:"balance"`
} UserOrdersRequest {
ID int64 `path:"id"`
} UserOrdersReply {
ID string `json:"id"`
State uint32 `json:"state"`
CreateAt string `json:"create_at"`
}
) service mall-api {
@handler UserHandler
get /user/:id (UserRequest) returns (UserReply) @handler UserOrdersHandler
get /user/:id/orders (UserOrdersRequest) returns (UserOrdersReply)
}

订单模块 API 定义

  • 获取订单详情
  • 生成订单
syntax = "v1"

type (
OrderRequest {
ID string `path:"id"`
} OrderReply {
ID string `json:"id"`
State uint32 `json:"state"`
CreateAt string `json:"create_at"`
} OrderCreateRequest {
ProductID int64 `json:"product_id"`
} OrderCreateReply {
Code int `json:"code"`
}
) service mall-api {
@handler OrderHandler
get /order/:id (OrderRequest) returns (OrderReply) @handler OrderCreateHandler
post /order/create (OrderCreateRequest) returns (OrderCreateReply)
}

商品模块 API 定义

  • 查看商品详情
syntax = "v1"

type ProductRequest {
ID int64 `path:"id"`
} type ProductReply {
ID int64 `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Count int64 `json:"count"`
} service mall-api {
@handler ProductHandler
get /product/:id (ProductRequest) returns (ProductReply)
}

生成单体服务

已经定义好了 API,接下来用 API 生成服务就会变得非常简单,我们使用 goctl 生成单体服务代码。

$ goctl api go -api api/mall.api -dir .

生成的代码结构如下:

.
├── api
│   ├── mall.api
│   ├── order.api
│   ├── product.api
│   └── user.api
├── etc
│   └── mall-api.yaml
├── internal
│   ├── config
│   │   └── config.go
│   ├── handler
│   │   ├── ordercreatehandler.go
│   │   ├── orderhandler.go
│   │   ├── producthandler.go
│   │   ├── routes.go
│   │   ├── userhandler.go
│   │   └── userordershandler.go
│   ├── logic
│   │   ├── ordercreatelogic.go
│   │   ├── orderlogic.go
│   │   ├── productlogic.go
│   │   ├── userlogic.go
│   │   └── userorderslogic.go
│   ├── svc
│   │   └── servicecontext.go
│   └── types
│   └── types.go
└── mall.go

解释一下生成的代码结构:

  • api:存放 API 描述文件
  • etc:用来定义项目配置,所有的配置项都可以写在 mall-api.yaml
  • internal/config:服务的配置定义
  • internal/handlerAPI 文件中定义的路由对应的 handler 的实现
  • internal/logic:用来放每个路由对应的业务逻辑,之所以区分 handlerlogic 是为了让业务处理部分尽可能减少依赖,把 HTTP requests 和逻辑处理代码隔离开,便于后续拆分成 RPC service
  • internal/svc:用来定义业务逻辑处理的依赖,我们可以在 main 函数里面创建依赖的资源,然后通过 ServiceContext 传递给 handlerlogic
  • internal/types:定义了 API 请求和返回数据结构
  • mall.gomain 函数所在文件,文件名和 API 定义中的 service 同名,去掉了后缀 -api

生成的服务不需要做任何修改就可以运行:

$ go run mall.go
Starting server at 0.0.0.0:8888...
实现业务逻辑

接下来我们来一起实现一下业务逻辑,出于演示目的逻辑会比较简单,并非真正业务逻辑。

首先,我们先来实现用户获取所有订单的逻辑,因为在用户模块并没有订单相关的信息,所以我们需要依赖订单模块查询用户的订单,所以我们在 UserOrdersLogic 中添加对 OrderLogic 依赖

type UserOrdersLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
orderLogic *OrderLogic
} func NewUserOrdersLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserOrdersLogic {
return &UserOrdersLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
orderLogic: NewOrderLogic(ctx, svcCtx),
}
}

OrderLogic 中实现根据 用户id 查询所有订单的方法

func (l *OrderLogic) ordersByUser(uid int64) ([]*types.OrderReply, error) {
if uid == 123 {
// It should actually be queried from database or cache
return []*types.OrderReply{
{
ID: "236802838635",
State: 1,
CreateAt: "2022-5-12 22:59:59",
},
{
ID: "236802838636",
State: 1,
CreateAt: "2022-5-10 20:59:59",
},
}, nil
} return nil, nil
}

UserOrdersLogicUserOrders 方法中调用 ordersByUser 方法

func (l *UserOrdersLogic) UserOrders(req *types.UserOrdersRequest) (*types.UserOrdersReply, error) {
orders, err := l.orderLogic.ordersByUser(req.ID)
if err != nil {
return nil, err
} return &types.UserOrdersReply{
Orders: orders,
}, nil
}

这时候我们重新启动 mall-api 服务,在浏览器中请求获取用户所有订单接口

http://localhost:8888/user/123/orders

返回结果如下,符合我们的预期

{
"orders": [
{
"id": "236802838635",
"state": 1,
"create_at": "2022-5-12 22:59:59"
},
{
"id": "236802838636",
"state": 1,
"create_at": "2022-5-10 20:59:59"
}
]
}

接下来我们再来实现创建订单的逻辑,创建订单首先需要查看该商品的库存是否足够,所以在订单模块中需要依赖商品模块。

type OrderCreateLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
productLogic *ProductLogic
} func NewOrderCreateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *OrderCreateLogic {
return &OrderCreateLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
productLogic: NewProductLogic(ctx, svcCtx),
}
}

创建订单的逻辑如下

const (
success = 0
failure = -1
) func (l *OrderCreateLogic) OrderCreate(req *types.OrderCreateRequest) (*types.OrderCreateReply, error) {
product, err := l.productLogic.productByID(req.ProductID)
if err != nil {
return nil, err
} if product.Count > 0 {
return &types.OrderCreateReply{Code: success}, nil
} return &types.OrderCreateReply{Code: failure}, nil
}

依赖的商品模块逻辑如下

func (l *ProductLogic) Product(req *types.ProductRequest) (*types.ProductReply, error) {
return l.productByID(req.ID)
} func (l *ProductLogic) productByID(id int64) (*types.ProductReply, error) {
return &types.ProductReply{
ID: id,
Name: "apple watch 3",
Price: 3333.33,
Count: 99,
}, nil
}

以上可以看出使用 go-zero 开发单体服务还是非常简单的,有助于我们快速开发上线,同时我们还做了模块的划分,为以后做微服务的拆分也打下了基础。

总结

通过以上的示例可以看出使用 go-zero 实现单体服务非常简单,只需要定义 api 文件,然后通过 goctl 工具就能自动生成项目代码,我们只需要在logic中填写业务逻辑即可,这里只是为了演示如何基于 go-zero 快速开发单体服务并没有涉及数据库和缓存的操作,其实我们的 goctl 也可以一键生成 CRUD 代码和 cache 代码,对于开发单体服务来说可以起到事半功倍的效果。

并且针对不同的业务场景,定制化的需求也可以通过自定义模板来实现,还可以在团队内通过远程 git 仓库共享自定义业务模板,可以很好的实现团队协同。

项目地址

https://github.com/zeromicro/go-zero

欢迎使用 go-zerostar 支持我们!

微信交流群

关注『微服务实践』公众号并点击 交流群 获取社区群二维码。

如果你有 go-zero 的使用心得文章,或者源码学习笔记,欢迎通过公众号联系投稿!

用 Go 快速开发一个 RESTful API 服务的更多相关文章

  1. FastAPI 快速搭建一个REST API 服务

    最近正好在看好的接口文档方便的工具, 突然看到这个, 试了一下确实挺方便 快速示例 from fastapi import FastAPI from pydantic import BaseModel ...

  2. 基于SpringBoot开发一个Restful服务,实现增删改查功能

    前言 在去年的时候,在各种渠道中略微的了解了SpringBoot,在开发web项目的时候是如何的方便.快捷.但是当时并没有认真的去学习下,毕竟感觉自己在Struts和SpringMVC都用得不太熟练. ...

  3. 通过beego快速创建一个Restful风格API项目及API文档自动化

    通过beego快速创建一个Restful风格API项目及API文档自动化 本文演示如何快速(一分钟内,不写一行代码)的根据数据库及表创建一个Restful风格的API项目,及提供便于在线测试API的界 ...

  4. 通过beego快速创建一个Restful风格API项目及API文档自动化(转)

    通过beego快速创建一个Restful风格API项目及API文档自动化 本文演示如何快速(一分钟内,不写一行代码)的根据数据库及表创建一个Restful风格的API项目,及提供便于在线测试API的界 ...

  5. 快速创建Flask Restful API项目

    前言 Python必学的两大web框架之一Flask,俗称微框架.它只需要一个文件,几行代码就可以完成一个简单的http请求服务. 但是我们需要用flask来提供中型甚至大型web restful a ...

  6. 使用python的Flask实现一个RESTful API服务器端[翻译]

    最近这些年,REST已经成为web services和APIs的标准架构,很多APP的架构基本上是使用RESTful的形式了. 本文将会使用python的Flask框架轻松实现一个RESTful的服务 ...

  7. 使用CodeIgniter框架搭建RESTful API服务

    使用CodeIgniter框架搭建RESTful API服务 发表于 2014-07-12   |   分类于 翻译笔记   |   6条评论 在2011年8月的时候,我写了一篇博客<使用Cod ...

  8. 使用python的Flask实现一个RESTful API服务器端

    使用python的Flask实现一个RESTful API服务器端 最近这些年,REST已经成为web services和APIs的标准架构,很多APP的架构基本上是使用RESTful的形式了. 本文 ...

  9. 转:使用python的Flask实现一个RESTful API服务器端

    提示:可以学习一下flask框架中对于密码进行校验的部分.封装了太多操作. 最近这些年,REST已经成为web services和APIs的标准架构,很多APP的架构基本上是使用RESTful的形式了 ...

随机推荐

  1. IE中的编码位置

    进入设置 找到Editor 找到File Encodings

  2. React系列——websocket群聊系统在react的实现

    前奏 这篇文章仅对不熟悉在react中使用socket.io的人.以及websocket入门者有帮助. 下面这个动态图展示的聊天系统是用react+express+websocket搭建的,很模糊吧, ...

  3. idea 提示错误: 找不到或无法加载主类

    首先检查自己的jdk 配置是否正确,检查好遍发现没有问题,但是项目就是运行不起来...... 重启idea,问题解决.

  4. 使用 ssm 实现登录日志记录

    使用 ssm 实现登录日志记录 学习总结 一.基础准备 1. 实现效果 2. 数据表 2.1 登陆日志信息表 2.3 员工表 二.代码实现 1. SysLogLogin 实体类 2. LogAspec ...

  5. 小程序tab栏可滑动,可点击居中demo

    效果图: 代码: <view class="container"> <!-- tab导航栏 --> <!-- scroll-left属性可以控制滚动条 ...

  6. EMS导入导出邮箱

    Exchange支持EMS命令导出用户邮箱内容作为备份的功能.当重要用户的邮件误删除后,可以通过导出的邮箱恢复数据. 1.授权管理用户 Exchange默认安装完成后,内置"Mailbox ...

  7. vim 下几种比较省劲的方式(vi结合着用)

    Vim的几种模式 正常模式:可以使用快捷键命令,或按:输入命令行. 插入模式:可以输入文本,在正常模式下,按i.a.o等都可以进入插入模式. 可视模式:正常模式下按v可以进入可视模式, 在可视模式下, ...

  8. java获取登录ip和地址

    //获取HttpServletRequest对象 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestCon ...

  9. Codeforces Round #719 (Div. 3) C. Not Adjacent Matrix

    地址 Problem - C - Codeforces 题意 每个格子,该格子和相邻的格子的值不能相同 题解 思维题, 先从1~n输出奇数,再输出偶数 代码 #include <iostream ...

  10. 前端vue之属性指令、style和class、条件渲染、列表渲染、事件处理、数据双向绑定、表单控制、v-model进阶

    今日内容概要 属性指令 style和class 条件渲染 列表渲染 事件处理 数据的双向绑定 v-model进阶 购物车案例 内容详细 1.属性指令 <!DOCTYPE html> < ...