您现在的位置是:首页 >学无止境 >【Go微服务开发】gin+grpc+etcd 重构 grpc-todolist 项目网站首页学无止境

【Go微服务开发】gin+grpc+etcd 重构 grpc-todolist 项目

小生凡一 2024-06-17 11:19:28
简介【Go微服务开发】gin+grpc+etcd 重构 grpc-todolist 项目

写在前面

最近稍微重构了之前写的 grpc-todolist 模块
项目地址:https://github.com/CocaineCong/grpc-todoList

1. 项目结构改变

与之前的目录有很大的区别

1.1 grpc_todolist 项目总体

1.1.1 改变前

grpc-todolist/
├── api-gatway  // 网关模块
├── task		// task模块
└── user		// user模块

v1版本的项目结构是分成了三个模块,网关,task,user模块。每个模块下各自读取配置文件,各自进行服务注册。

http 请求进入网关之后,网关开始转发并调用 rpc 请求,user、task模块接受到 rpc 调用之后,开始进行操作业务逻辑处理,结果返回给网关,网关再次对客户端进行 resp 响应。

基本思想没有问题,但是这种结构的问题是重复代码过多,很多代码是可以复用的,就没必要都写。

1.1.2 改变后

我们抽离出各个微服务模块下相同的内容,比如config,pkg,proto的内容,并且新建一个app文件夹,放置所有的gateway、task、user模块中所独有的,并且启动文件都放到各个模块下的cmd中。

grpc-todolist/
├── app                   // 各个微服务
│   ├── gateway           // 网关
│   ├── task              // 任务模块微服务
│   └── user              // 用户模块微服务
├── bin                   // 编译后的二进制文件模块
├── config                // 配置文件
├── consts                // 定义的常量
├── doc                   // 接口文档
├── idl                   // protoc文件
│   └── pb                // 放置生成的pb文件
├── logs                  // 放置打印日志模块
├── pkg                   // 各种包
│   ├── e                 // 统一错误状态码
│   ├── discovery         // etcd服务注册、keep-alive、获取服务信息等等
│   ├── res               // 统一response接口返回
│   └── util              // 各种工具、JWT、Logger等等..
└── types                 // 定义各种结构体

1.2 gateway网关模块

网关模块单单只是处理http请求,没有任何的业务逻辑,所以基本都是大量的中间件的使用。比如jwt鉴权,限流,etcd的服务发现等等…

gateway/
├── cmd                   // 启动入口
├── internal              // 业务逻辑(不对外暴露)
│   ├── handler           // 视图层
│   └── service           // 服务层
│       └── pb            // 放置生成的pb文件
├── logs                  // 放置打印日志模块
├── middleware            // 中间件
├── routes                // http 路由模块
└── rpc                   // rpc 调用

1.3. 各微服务模块

各个微服务的结构比较简单,因为各微服务模块都是处于一种被调用的状态,在该模块下聚焦好业务即可。

user/
├── cmd                    // 启动入口
└── internal               // 业务逻辑(不对外暴露)
    ├── service            // 业务服务
    └── repository         // 持久层
        └── db             // 视图层
          ├── dao          // 对数据库进行操作
          └── model        // 定义数据库的模型

1.4 项目的总结模块

  • 抽离出 proto 成 idl ,抽离pkg,config等公共模块。
  • 简化各个微服务模块的结构。

2. 代码层级的改变

2.1 RPC的调用方式

2.1.1 改变前

在v1版本中,我们是将我们的微服务的服务实例放到 gin.Key

func InitMiddleware(service []interface{}) gin.HandlerFunc {
	return func(context *gin.Context) {
		// 将实例存在gin.Keys中
		context.Keys = make(map[string]interface{})
		context.Keys["user"] = service[0]
		context.Keys["task"] = service[1]
		context.Next()
	}
}

通过断言的方式取出这个服务实例

func UserRegister(ginCtx *gin.Context) {
	var userReq service.UserRequest
	PanicIfUserError(ginCtx.Bind(&userReq))
	// 从gin.Key中取出服务实例
	userService := ginCtx.Keys["user"].(service.UserServiceClient)
	userResp, err := userService.UserRegister(context.Background(), &userReq)
	PanicIfUserError(err)
	r := res.Response{
		Data:   userResp,
		Status: uint(userResp.Code),
		Msg:    e.GetMsg(uint(userResp.Code)),
	}
	ginCtx.JSON(http.StatusOK, r)
}

这种结构是没有什么问题,但是就是缺少 rpc调度 那个味道。

2.1.2 改变后

我们可以新建一个rpc的文件来存储rpc相关,例如下面这个代码,是对下游的 UserRegister 进行调用。

func UserRegister(ctx context.Context, req *userPb.UserRequest) (resp *userPb.UserCommonResponse, err error) {
	r, err := UserClient.UserRegister(ctx, req)
	if err != nil {
		return
	}

	if r.Code != e.SUCCESS {
		err = errors.New(r.Msg)
		return
	}

	return
}

然后在 handler 这里调用 rpc 进行操作。而不需要把服务实例放到ctx中,去取服务实例来进行调用。

func UserRegister(ctx *gin.Context) {
	var userReq pb.UserRequest
	if err := ctx.Bind(&userReq); err != nil {
		ctx.JSON(http.StatusBadRequest, ctl.RespError(ctx, err, "绑定参数错误"))
		return
	}
	r, err := rpc.UserRegister(ctx, &userReq)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, ctl.RespError(ctx, err, "UserRegister RPC服务调用错误"))
		return
	}

	ctx.JSON(http.StatusOK, ctl.RespSuccess(ctx, r))
}

2.2 Makefile文件编写

makefile文件是起到项目的快速启动和关闭的作用

2.2.1 proto文件的快速生成

这里使用protocprotoc-go-inject-tag的命令来生成.pb.go文件。

.PHONY: proto
proto:
	@for file in $(IDL_PATH)/*.proto; do 
		protoc -I $(IDL_PATH) $$file --go-grpc_out=$(IDL_PATH)/pb --go_out=$(IDL_PATH)/pb; 
	done
	@for file in $(shell find $(IDL_PATH)/pb/* -type f); do 
		protoc-go-inject-tag -input=$$file; 
	done

protoc命令:用于生成pb.go,grpc.go文件。
protoc-go-inject-tag 命令:用于重写pb.go文件中的tag,使得能加入json:“xxx” form:"xxx"等tag来进行操作。

例如如下proto文件,如果没有 protoc-go-inject-tag 那么我们生成的 NickName 中就是大写,也就是接受参数是大写,但我们一般是使用小写来接受参数。

message UserRequest{
  // @inject_tag: json:"nick_name" form:"nick_name" uri:"nick_name"
  string NickName=1;
  // @inject_tag: json:"user_name" form:"user_name" uri:"user_name"
  string UserName=2;
  // @inject_tag: json:"password" form:"password" uri:"password"
  string Password=3;
  // @inject_tag: json:"password_confirm" form:"password_confirm" uri:"password_confirm"
  string PasswordConfirm=4;
}

当然,除了这个解决方法我们可以将NickName写成nickname小写,也是可以的,protoc会自动在代码层面,将nickname变成Nickname,代码层面还是可以调用的,并且接受参数还可以是小写。例如这样

message UserRequest{
  string nick_name=1;
  string user_name=2;
  string password=3;
  string password_confirm=4;
}

看个人喜好吧…

2.2.2 环境的快速启动

快速启动环境

.PHONY: env-up
env-up:
	docker-compose up -d

这里会根据compose文件,来进行环境的快速启动。

在这里插入图片描述
创建的时候,已经是创建了数据库了,所以我们只需要去到各个模块下的cmd文件夹下进行启动微服务即可,例如到 app/user/cmd,启动这个main.go就可以启动user模块了。

快速关闭环境

.PHONY: env-down
env-down:
	docker-compose down

在这里插入图片描述

以上就是这次grpc重构过程中的重要更新,大家可以把项目clone下来,自己跑跑,切换一下v1,v2分支来进行感受这一次的改变。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。