何时使用单体 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. SpringCloud个人笔记-01-Eureka初体验

    eureka是一个高可用的组件,它没有后端缓存,每一个实例注册之后需要向注册中心发送心跳,在默认情况下erureka server也是一个eureka client ,必须要指定一个 serve &l ...

  2. PCB布线总的原则

    转自张飞实战电子公众号 PCB布线总的原则 最短路径和减少干扰 PCB布线的总的流程大致如下: 1了解制造厂商的制造规范-线宽,线间距,过孔要求及层数要求: 2确定层数并定义各层的功能: 3设计布线规 ...

  3. Linux网络配置:Nat和桥接模式详解

    Linux网络配置:Nat和桥接模式详解 一.我们首先说一下VMware的几个虚拟设备: Centos虚拟网络编辑器中的虚拟交换机: VMnet0:用于虚拟桥接网络下的虚拟交换机: VMnet1:用于 ...

  4. Architecture Review Board

    Architecture Review Board What's an Architecture Review? Architecture design is not a one-time final ...

  5. css写作建议和性能优化小结

    1.前言 还有几天就到国庆中秋了,快要放假了,先祝大家节日快乐!之前写过js的写作建议和技巧,那么今天就来聊聊css吧!说到css,每一个网页都离不开css,但是对于css,很多开发者的想法就是,cs ...

  6. 媒体查询@media的使用

    媒体查询 参考:https://developer.mozilla.org...一个媒体查询由一个可选的媒体类型和零个或多个使用媒体功能的限制了样式表范围的表达式组成,例如宽度.高度和颜色.媒体查询, ...

  7. WebView的一些简单用法

    一直想写一个关于 WebView 控件的 一些简单运用,都没什么时间,这次也是挤出时间写的,里面的一些基础知识就等有时间再更新讲解一下,今天就先把项目出来做一些简单介绍,过多的内容可以看我的源码,都传 ...

  8. Mybatis-Dao层实现(通过代理方式)

    1.代理方式开发是主流 2.Mapper接口开发方法只需要编写Mapper接口(相当于Dao接口),然后由Mybatis根据接口创建动态代理对象 Mapper接口开发需要遵循以下规范 一一对应 Use ...

  9. LC-977

    给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序. 示例 1: 输入:nums = [-4,-1,0,3,10] 输出:[0,1,9,1 ...

  10. git远程建立仓库后,将本地项目推到远程报错 fatal: refusing to merge unrelated histories

    出现这个问题的最主要原因还是在于本地仓库和远程仓库实际上是独立的两个仓库,假如之前是直接clone的方式在本地仓库就不会有这个问题了. 解决方式是在命令后紧跟 --allow-unrelated-hi ...