Go快速开发一个RESTful API服务
作者:万俊峰Kevin 发布时间:2024-04-30 10:05:07
何时使用单体 RESTful 服务
对于很多初创公司来说,业务的早期我们更应该关注于业务价值的交付,而单体服务具有架构简单,部署简单,开发成本低等优点,可以帮助我们快速实现产品需求。我们在使用单体服务快速交付业务价值的同时,也需要为业务的发展预留可能性,所以我们一般会在单体服务中清晰的拆分不同的业务模块。
商城单体 RESTful 服务
我们以商城为例来构建单体服务,商城服务一般来说相对复杂,会由多个模块组成,比较重要的模块包括账号模块、商品模块和订单模块等,每个模块会有自己独立的业务逻辑,同时每个模块间也会相互依赖,比如订单模块和商品模块都会依赖账号模块,在单体应用中这种依赖关系一般是通过模块间方法调用来完成。一般单体服务会共享存储资源,比如 MySQL 和 Redis 等。
单体服务的整体架构比较简单,这也是单体服务的优点,客户请求通过 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.api 和 mall.api,其中 mall.api 为聚合的 api 文件,通过 import 导入,文件列表如下:
api
|-- mall.api
|-- order.api
|-- product.api
|-- user.api
Mall API 定义
mall.api 的定义如下,其中 syntax = “v1” 表示这是 zero-api 的 v1 语法
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/handler:API 文件中定义的路由对应的 handler 的实现
internal/logic:用来放每个路由对应的业务逻辑,之所以区分 handler 和 logic 是为了让业务处理部分尽可能减少依赖,把 HTTP requests 和逻辑处理代码隔离开,便于后续拆分成 RPC service
internal/svc:用来定义业务逻辑处理的依赖,我们可以在 main 函数里面创建依赖的资源,然后通过 ServiceContext 传递给 handler 和 logic
internal/types:定义了 API 请求和返回数据结构
mall.go:main 函数所在文件,文件名和 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
}
在 UserOrdersLogic 的 UserOrders 方法中调用 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 开发单体服务还是非常简单的,有助于我们快速开发上线,同时我们还做了模块的划分,为以后做微服务的拆分也打下了基础。
来源:https://juejin.cn/post/7098137538644148254
猜你喜欢
- 今天跟大家分享下selenium中根据父子、兄弟、相邻节点定位的方法,很多人在实际应用中会遇到想定位的节点无法直接定位,需要通过附近节点来相
- 1.Django框架Django是一个开放源代码的Web应用框架,由Python写成。它采用 了MVC的框架模式,即模型(M)、视图(V)和
- 本文实例为大家分享了python实现抠图给证件照换背景的具体代码,供大家参考,具体内容如下import cv2import numpy as
- SQL Server Sa用户相信大家都有一定的理解,下面就为您介绍SQL Server 2000身份验证模式的修改方法及SQL Serve
- 一、MySQL备份类型详解对于现代互联网公司而言,存储在服务器数据库中的数据,逐步成为企业和公司的命脉,对企业和公司的生存发展具有十分重大的
- 借助 org.springframework.ui.Model 对象或 Map 对象将信息传到 springmvc 的页面中需要:jstl
- 一般采用的方法:self.window = Qdialog() # 实例化self.window.show() # 显示界面用这种方法只能打
- 遍历指定文件夹下的文件,根据文件后缀名,获取指定类型的文件列表;根据文件列表里的文件路径,逐个获取文件属性里的“修改时间”,如果“修改时间”
- DataFrame对象本质上是带有行列索引的二维矩阵,所以欲对DataFrame对象进行转置操作,需要交换行列索引,同时使二维矩阵转置。首先
- 实这本是说明一个问题 : 每个人在提高自己能力这件事情上, 需要持续不断地努力。以最典型的例子来看,只有通过学习,程序员才能保证不断进步。
- 第一个版本在这个版本中,首先创建了 RouterConfig 对象,其构造方法创建了 tornado.web.Application() 并
- 介绍最近在项目中遇到插入数据瓶颈,几万、几十万、几百万的数据保存到MYSQL数据库,使用EF插入数据速度非常慢,数据量非常大时EF插入需要几
- 第一种方法:第一步:先看报错窗口2003 can't connect to MySQL server on '127.0.0
- 测试过程如下:create table sales as select * f
- SQL Server数据库查询优化的常用方法总结:本文中,abigale代表查询字符串,ada代表数据表名,alice代表字段名。技巧一:问
- 一、python压缩模块简介python直接通过内置压缩模块可以直接进行压缩文件的创建;内置模块 zipfile/rarfile 完成压缩文
- 正则表达式处理花括号内容替换赋值@Test public void replaceStr() { &
- 在实际使用python时,我们会将一些公共的东西写到一些基础模块中,供其他模块去调用,这时会去import自定义的一些基础模块,然后来导入。
- 一.序单从库名大概就能猜出其作用。sync.Once使用起来很简单, 下面是一个简单的使用案例package mainimport (&qu
- 我们在进行程序操作的时候,因为各种原因,需要通过不同的形式返回到之前的对象。不知道小伙伴们会几种返回的函数方法呢?今天要介绍的是findal