0%

go-zero实战系列一

说明

本次主要按照这个博主的go-zero微服务实战进行学习

安装依赖

  • 安装依赖go-zero: go get -u github.com/zeromicro/go-zero@latest
  • 安装依赖goctl,用命令进行安装:go install github.com/zeromicro/go-zero/tools/goctl@latest
  • 安装成功后,输入go env 找到GOPATH
1
2
3
4
5
E:\proj\gowork\studyGoZero>go env
set GO111MODULE=on
set GOPATH=C:\Users\Administrator\go
set GOROOT=E:\app\Go
....
  • C:\Users\Administrator\go\bin\goctl.exe 拷贝到GOROOT\bin目录下
  • 查看版本成功
1
2
PS E:\proj\gowork\studyGoZero\apps\order> goctl --version
goctl version 1.5.4 windows/amd64
  • 安装protoc ,查看他的作用
1
goctl env check --install --verbose --force
  • 安装protoc 成功后,把C:\Users\Administrator\go\bin\ 的相关文件拷贝到GOROOT\bin

服务划分

image-20230721102332169

从以上思维导图可以看出整个电商系统功能还是比较多的,我们根据业务职能做如下微服务的划分:

  • 商品服务(product) - 商品的添加、信息查询、库存管理等功能
  • 购物车服务(cart) - 购物车的增删改查
  • 订单服务(order) - 生成订单,订单管理
  • 支付服务(pay) - 通过调用第三方支付实现支付功能
  • 账号服务(user) - 用户信息、等级、封禁、地址管理
  • 推荐服务(recommend) - 首页商品推荐
  • 评论服务(reply) - 商品的评论功能、评论的回复功能

BFF层

  • 但对于一个复杂的高并发的系统来说,我们需要处理各种异常的场景,比如某个页面需要依赖多个微服务提供的数据,为了避免串行请求导致的耗时过长,我们一般会并行的请求多个微服务,这个时候其中的某个服务请求异常的话我们可能需要做一些特殊的处理,比如提供一些降级的数据等。我们的解决方案就是加一层,即BFF层,通过BFF对外提供HTTP接口,客户端只与BFF进行交互,我们的这个项目为了简化只会采用一个BFF服务

image-20230721102651764

  • 我们可以提供多个BFF吗?答案是当然可以。BFF的目的是为客户端提供一个集中的接口,例如移动端页面和浏览器页面的数据协议不同,这种情况下为了更好的表示数据,可以使用两个BFF,同时只供一个BFF如果该BFF异常就会导致所有的业务受影响,提供多个BFF也可以提高服务的可用性,降低业务异常的影响面。多个BFF架构图如下:

image-20230721102751280

项目结构

  • 初始化项目
1
2
3
4
cd E:\proj\gowork
mkdir studyGoZero
cd studyGoZero
go mod init example.com/studyGoZero
  • 目录结构如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
─apps
│ ├─app
│ │ └─api
│ ├─cart
│ │ ├─admin
│ │ ├─rmq
│ │ └─rpc
│ ├─order
│ │ ├─admin
│ │ ├─rmq
│ │ └─rpc
│ ├─paly
│ │ ├─admin
│ │ ├─rmq
│ │ └─rpc
│ ├─product
│ │ ├─admin
│ │ ├─rmq
│ │ └─rpc
│ ├─recommend
│ │ ├─admin
│ │ ├─rmq
│ │ └─rpc
│ ├─reply
│ │ ├─admin
│ │ ├─rmq
│ │ └─rpc
│ └─user
│ ├─admin
│ ├─rmq
│ └─rpc
└─pkg
  • 其中apps存放的是我们所有的微服务,比如order为订单相关的微服务,pkg目录为所有服务共同依赖的包的存放路径,比如所有的服务都需要依赖鉴权就可以放到pkg目录下
  • app - BFF服务
  • cart - 购物车服务
  • order - 订单服务
  • pay - 支付服务
  • product - 商品服务
  • recommend - 推荐服务
  • reply - 评论服务
  • user - 账号服务

在每个服务目录下我们又会分为多个服务,主要会有如下几类服务:

  • api - 对外的BFF服务,接受来自客户端的请求,暴露HTTP接口
  • rpc - 对内的微服务,仅接受来自内部其他微服务或者BFF的请求,暴露gRPC接口
  • rmq - 负责进行流式任务处理,上游一般依赖消息队列,比如kafka等
  • admin - 也是对内的服务,区别于rpc,更多的是面向运营侧的且数据权限较高,通过隔离可带来更好的代码级别的安全,直接提供HTTP接口

API 定义

首页API

首页功能主要分为四个部分,搜索、Banner图、限时抢购和推荐商品列表,点击搜索框会跳转到搜索页,推荐部分是分页展示的,用户通过不断地往上滑动可以加载下一页。通过分析首页我们大致需要提供三个接口,分别是Banner接口,限时抢购接口和推荐接口。

image-20230720110620328

  • 打开apps\app\api\api.api文件,写入对外api的代码定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
syntax = "v1"
type (
// 首页的banner响应请求
HomeBannerResponse {
Banners []*Banner `json:"banners"`
}
.....

)

service api-api {

@doc "首页Banner"
@handler HomeBannerHandler
get /v1/home/banner returns (HomeBannerResponse)

@doc "限时抢购"
@handler FlashSaleHandler
get /v1/flashsale returns (FlashSaleResponse)

@doc "推荐商品列表"
@handler RecommendHandler
get /v1/recommend (RecommendRequest) returns (RecommendResponse)

@doc "分类商品列表"
@handler CategoryListHandler
get /v1/category/list (CategoryListRequest) returns (CategoryListResponse)
@doc "购物车列表"
@handler CartListHandler
get /v1/cart/list (CartListRequest) returns (CartListResponse)

@doc "商品评论列表"
@handler ProductCommentHandler
get /v1/product/comment (ProductCommentRequest) returns (ProductCommentResponse)

@doc "订单列表"
@handler OrderListHandler
get /v1/order/list (OrderListRequest) returns (OrderListResponse)

@doc "商品详情"
@handler ProductDetailHandler
get /v1/product/detail (ProductDetailRequest) returns (ProductDetailResponse)
}
  • 定义好api后,我们使用如下命令重新生成项目代码,输出如下信息表明生成成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
PS E:\proj\gowork\studyGoZero\apps\app\api> goctl api go -api api.api -dir .
Done.

// 查看目录
PS E:\proj\gowork\studyGoZero\apps\app\api> tree /f
E:.
│ api.api
│ api.go

├─etc
│ api-api.yaml

└─internal
├─config
│ config.go

├─handler
│ cartlisthandler.go
│ categorylisthandler.go
│ flashsalehandler.go
│ homebannerhandler.go
│ orderlisthandler.go
│ productcommenthandler.go
│ productdetailhandler.go
│ recommendhandler.go
│ routes.go

├─logic
│ cartlistlogic.go
│ categorylistlogic.go
│ flashsalelogic.go
│ homebannerlogic.go
│ orderlistlogic.go
│ productcommentlogic.go
│ productdetaillogic.go
│ recommendlogic.go

├─svc
│ servicecontext.go

└─types
types.go

admin 层代码

order目录

  • 目录为: E:\proj\gowork\studyGoZero\apps\order

  • 执行如下命令即可初始化order admin代码,注意order adminapi服务,直接对前端提供HTTP接口

    1
    E:\proj\gowork\studyGoZero\apps\order> goctl api new admin
  • 代码结构如下

1
2
3
4
5
6
7
─admin
│ │ admin.api
│ │ admin.go
│ │
│ ├─etc
│ │ admin-api.yaml
│ │
  • 生成的服务代码我们可以直接运行go run admin.go,默认侦听在8888端口

    1
    2
    PS E:\proj\gowork\studyGoZero\apps\order\admin> go run .\admin.go 
    Starting server at 0.0.0.0:8888...
  • 对于rmq服务我们会使用go-zero提供的 kq 功能,这里先初始化main.go

其他层

  • pay、pruduct、recommend、reply、user 等按照同样的方式进行初始化

rpc服务层

  • Goctl Rpc是goctl脚手架下的一个rpc服务代码生成模块,支持proto模板生成和rpc服务代码生成,通过此工具生成代码你只需要关注业务逻辑编写而不用去编写一些重复性的代码

  • 作者博客中初始化rpc使用的这种 goctl rpc new rpc ,并不推荐,研究代码后发现和作者提供的仓库代码不一致,使用的第二种方式生成的rpc服务层的代码和目录:通过指定proto生成rpc服务

  • 因为BFF只负责数据的组装工作,数据真正的来源是各个微服务通过RPC接口提供,接下来我们来定义各个微服务的proto。如下展示的订单列表页面由两部分数据组成,分别是订单数据和商品数据,也就是我们的BFF需要依赖order-rpc和product-rpc来完成该页面数据的组装,下面我们分别来定义order-rpc和product-rpc

image-20230725100643383

order层

  • E:\proj\gowork\studyGoZero\apps\order\rpc\order.proto,定义如下,service名字为Order,添加了Orders获取订单列表rpc接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
syntax = "proto3";

package order;
option go_package = "./order";


service Order {
rpc Orders(OrdersRequest) returns(OrdersResponse);
rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
rpc CreateOrderCheck(CreateOrderRequest) returns (CreateOrderResponse);
rpc RollbackOrder (CreateOrderRequest) returns (CreateOrderResponse);
rpc CreateOrderDTM(AddOrderReq) returns (AddOrderResp);
rpc CreateOrderDTMRevert(AddOrderReq) returns(AddOrderResp);
rpc GetOrderById(GetOrderByIdReq) returns (GetOrderByIdResp);
}

....

使用如下命令重新生成代码,注意这里需要依赖protoc-gen-goprotoc-gen-go-grpc两个插件,木有安装的话执行下面命令会报错:

1
2
PS E:\proj\gowork\studyGoZero\apps\order\rpc>  goctl rpc protoc order.proto --go_out=. --go-grpc_out=. --zrpc_out=.
Done.

注意最终生成的服务端的代码目录为:E:\proj\gowork\studyGoZero\apps\order\rpc\orderclient\order.go

目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
PS E:\proj\gowork\studyGoZero\apps\order\rpc> tree /f
卷 软件盘 的文件夹 PATH 列表
卷序列号为 240F-1787
E:.
│ order.go
│ order.proto

├─etc
│ order.yaml

├─internal
│ ├─config
│ │ config.go
│ │
│ ├─logic
│ │ createorderchecklogic.go
│ │ createorderdtmlogic.go
│ │ createorderdtmrevertlogic.go
│ │ createorderlogic.go
│ │ getorderbyidlogic.go
│ │ orderslogic.go
│ │ rollbackorderlogic.go
│ │
│ ├─server
│ │ orderserver.go
│ │
│ └─svc
│ servicecontext.go

├─order
│ order.pb.go
│ order_grpc.pb.go

└─orderclient
order.go
  • 本地安装etcd,打开E:\app\etcd-v3.4.27-windows-amd64\etcd.exe启动服务

etcd是一个分布式一致性键值存储,其主要用于分布式系统的共享配置和服务发现。

etcd由Go语言编写

  • 生成好后然后启动order-rpc服务,需要连接etcd服务
1
2
3
4
5
PS E:\proj\gowork\studyGoZero\apps\order\rpc> go run .\order.go
Starting rpc server at 0.0.0.0:8080...
{"@timestamp":"2023-07-24T17:03:51.723+08:00","caller":"stat/usage.go:61","content":"CPU: 0m, MEMORY: Alloc=3.2Mi, TotalAlloc=6.1Mi, Sys=17.9Mi, NumGC=3","level":"stat"}
{"@timestamp":"2023-07-24T17:03:51.737+08:00","caller":"load/sheddingstat.go:61","content":"(rpc) shedding_stat [1m], cpu: 0, total: 0, pass: 0, drop: 0","level":"stat"}
{"@timestamp":"2023-07-24T17:04:51.711+08:00","caller":"stat/usage.go:61","content":

在E:\proj\gowork\studyGoZero\apps\order\rpc\etc\order.yaml中,可以看到etcd的连接服务器地址,和order-rpc启动的地址信息:

1
2
3
4
5
6
Name: order.rpc
ListenOn: 0.0.0.0:8080
Etcd:
Hosts:
127.0.0.1:2379
Key: order.rpc

product

E:\proj\gowork\studyGoZero\apps\product\rpc\product.proto,定义如下,service名字为Order,添加了product产品的rpc接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
syntax = "proto3";

package product;
option go_package = "./product";

service Product {
rpc Product(ProductItemRequest) returns (ProductItem) ;
rpc Products(ProductRequest) returns(ProductResponse);
rpc ProductList(ProductListRequest) returns(ProductListResponse);
rpc OperationProducts(OperationProductsRequest) returns (OperationProductsResponse);
rpc UpdateProductStock(UpdateProductStockRequest) returns (UpdateProductStockResponse);
rpc CheckAndUpdateStock(CheckAndUpdateStockRequest) returns (CheckAndUpdateStockResponse);
rpc CheckProductStock(UpdateProductStockRequest) returns (UpdateProductStockResponse);
rpc RollbackProductStock(UpdateProductStockRequest) returns (UpdateProductStockResponse);
rpc DecrStock(DecrStockRequest) returns(DecrStockResponse);
rpc DecrStockRevert(DecrStockRequest) returns(DecrStockResponse);
}
...

  • E:\proj\gowork\studyGoZero\apps\product\rpc\etc\product.yaml配置启动服务地址端口为8081
1
2
3
4
5
6
Name: product.rpc
ListenOn: 0.0.0.0:8081
Etcd:
Hosts:
- 127.0.0.1:2379
Key: product.rpc
  • 启动product-rpc服务,需要连接etcd服务
1
2
PS E:\proj\gowork\studyGoZero\apps\product\rpc> go run .\product.go
Starting rpc server at 0.0.0.0:8081...

BUFF 层配置

  • 因为我们的BFF需要依赖order.rpc和product.rpc,我们需要先添加配置文件(E:\proj\gowork\studyGoZero\apps\app\api\etc\api-api.yaml),如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Name: api-api
    Host: 0.0.0.0
    Port: 8888
    OrderRPC:
    Etcd:
    Hosts:
    - 127.0.0.1:2379
    Key: order.rpc
    ProductRPC:
    Etcd:
    Hosts:
    - 127.0.0.1:2379
    Key: product.rpc
  • 然后在E:\proj\gowork\studyGoZero\apps\app\api\internal\svc\servicecontext.go中添加RPC的客户端,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package svc

    import (
    "example.com/studyGoZero/apps/app/api/internal/config"
    "example.com/studyGoZero/apps/order/rpc/orderclient"
    "example.com/studyGoZero/apps/product/rpc/productclient"
    )

    type ServiceContext struct {
    Config config.Config
    OrderRPC orderclient.Order
    ProductRPC productclient.Product
    }

    func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
    Config: c,
    }
    }

最后只要在订单接口的E:\proj\gowork\studyGoZero\apps\app\api\internal\logic\orderlistlogic.go方法中添加逻辑就可以啦,这里只是演示,所以会比较简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func (l *OrderListLogic) OrderList(req *types.OrderListRequest) (resp *types.OrderListResponse, err error) {
orderRet, err := l.svcCtx.OrderRPC.Orders(l.ctx, &order.OrdersRequest{UserId: req.UID})
if err != nil {
return nil, err
}
var pids []string
for _, o := range orderRet.Orders {
pids = append(pids, strconv.Itoa(int(o.Id)))
}
productRet, err := l.svcCtx.ProductRPC.Products(l.ctx, &product.ProductRequest{ProductIds: strings.Join(pids, ",")})
if err != nil {
return nil, err
}
var orders []*types.Order
for _, o := range orderRet.Orders {
if p, ok := productRet.Products[o.Id]; ok {
orders = append(orders, &types.Order{
OrderID: o.Orderid,
ProductName: p.Name,
})
}
}
return &types.OrderListResponse{Orders: orders}, nil
}

测试

  • 请确保order、product 的rpc服务器已经启动
1
2
3
4
5
PS E:\proj\gowork\studyGoZero\apps\product\rpc> go run .\product.go
Starting rpc server at 0.0.0.0:8081...

PS E:\proj\gowork\studyGoZero\apps\order\rpc> go run .\order.go
Starting rpc server at 0.0.0.0:8080...
  • 启动api层服务
1
2
E:\proj\gowork\studyGoZero\apps\app\api>go run .\api.go
Starting rpc server at 0.0.0.0:8888...
  • 浏览器打开http://127.0.0.1:8888/v1/order/list?uid=123 发现报错空指针
1
"content":"(/v1/order/list?uid=123 - 127.0.0.1:57933) runtime error: invalid memory address or nil pointer dereference\ngoroutine 57 [running]:\nruntime/debug.Stack()\n\tE:/app/Go/src/runtime/debug/stack.go:24 +0x65\ngithub.com/zeromicro/go-zero/rest/handler.RecoverHandler.func1.1()\n\tC:/Users/Administrator/go/pkg/mod/github.com/zeromicro/go-zero@v1.5.4/rest/handler/recoverhandler.go:16 +0x66\npanic({0x1f56680, 0x331f9d0})\n\tE:/app/Go/src/runtime/panic.go:884 +0x213\nexample.com/studyGoZero/apps/app/api/internal/logic.(*OrderListLogic).OrderList(0xc0002cfcf8, 0xc00011e380)\n\tE:/proj/gowork/studyGoZero/apps/app/api/internal/logic/orderlistlogic.go:31
  • 虽然报错 了,但是可以知道整个服务已经是通的,因为现在并没有订单的任何数据

总结

  • api层- 对外的BFF服务。接受来自客户端的请求,暴露HTTP接口。本次实例启动了api.go,并且连接了order和product的rpc
  • api/api.api 为具体的对我暴露的请求
  • app\api\internal\logic 对应api/api.api为对外暴露请求的具体逻辑,具体代码又调用了order和product的rpc的逻辑

image-20230725145056412