[Go microservice development] gin+grpc+etcd refactoring grpc-todolist project

Write in front

Recently refactored the previously written grpc-todolist module slightly
Project address: https://github.com/CocaineCong/grpc-todoList

1. Project structure changes

There is a big difference from the previous catalog

1.1 grpc_todolist project overall

1.1.1 Before the change

grpc-todolist/
├── api-gatway // gateway module
├── task // task module
└── user // user module

The project structure of the v1 version is divided into three modules, gateway, task, and user modules. Each module reads the configuration file separately and registers the service separately.

After the http request enters the gateway, the gateway starts to forward and call the rpc request. After the user and task modules receive the rpc call, they start to process the operation business logic. The result is returned to the gateway, and the gateway responds to the client again with resp.

There is no problem with the basic idea, but the problem with this structure is thatthere is too much repetitive code, and many codes can be reused, so there is no need to write them all.

After 1.1.2 changes

We extract the same content under each microservice module, such as the content of config, pkg, proto, and create a new app folder, place all gateway, task, user modules Unique, and the startup files are placed in cmd under each module.

grpc-todolist/
├── app // each microservice
│ ├── gateway // gateway
│ ├── task // task module microservice
│ └── user // user module microservice
├── bin // compiled binary file module
├── config // configuration file
├── consts // defined constants
├── doc // interface documentation
├── idl // protoc file
│ └── pb // place the generated pb file
├── logs // Place the print log module
├── pkg // various packages
│ ├── e // unified error status code
│ ├── discovery // etcd service registration, keep-alive, obtaining service information, etc.
│ ├── res // Unified response interface returns
│ └── util // Various tools, JWT, Logger, etc..
└── types // define various structures

1.2 gateway module

The gateway module only processes http requests without any business logic, so it basically uses a lot of middleware. Such as jwt authentication, current limiting, etcd service discovery, etc…

gateway/
├── cmd // start entry
├── internal // business logic (not exposed externally)
│ ├── handler // view layer
│ └── service // service layer
│ └── pb // place the generated pb file
├── logs // Place the print log module
├── middleware // middleware
├── routes // http routing module
└── rpc // rpc call

1.3. Each microservice module

The structure of each microservice is relatively simple. Because each microservice module is in a state of being called, it is enough to focus on the business under this module.

user/
├── cmd // start entry
└── internal // business logic (not exposed externally)
    ├── service // business service
    └── repository // persistence layer
        └── db // view layer
          ├── dao // operate on the database
          └── model // define the model of the database

Summary module of 1.4 project

  • Extract proto into idl, and extract public modules such as pkg and config.
  • Simplify the structure of each microservice module.

2. Code level changes

2.1 RPC calling method

2.1.1 Before the change

In the v1 version, we put the service instance of our microservice in gin.Key

func InitMiddleware(service []interface{<!-- -->}) gin.HandlerFunc {<!-- -->
return func(context *gin.Context) {<!-- -->
// Store the instance in gin.Keys
context.Keys = make(map[string]interface{<!-- -->})
context.Keys["user"] = service[0]
context.Keys["task"] = service[1]
context. Next()
}
}

Take out the service instance by asserting

func UserRegister(ginCtx *gin.Context) {<!-- -->
var userReq service. UserRequest
PanicIfUserError(ginCtx. Bind( & amp; userReq))
// Get the service instance from 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)
}

There is nothing wrong with this structure, but it lacks the flavor of rpc scheduling.

After 2.1.2 changes

We can create a new rpc file to store rpc related, for example, the following code is to call the downstream 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
}

Then call rpc in the handler to operate. It is not necessary to put the service instance in ctx, and fetch the service instance to make the call.

func UserRegister(ctx *gin.Context) {<!-- -->
var userReq pb. UserRequest
if err := ctx.Bind( & amp;userReq); err != nil {<!-- -->
ctx.JSON(http.StatusBadRequest, ctl.RespError(ctx, err, "Binding parameter error"))
return
}
r, err := rpc. UserRegister(ctx, & userReq)
if err != nil {<!-- -->
ctx.JSON(http.StatusInternalServerError, ctl.RespError(ctx, err, "UserRegister RPC service call error"))
return
}

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

2.2 Makefile writing

makefile is used to quickly start and close the project

2.2.1 Rapid generation of proto files

Here the protoc and protoc-go-inject-tag commands are used to generate the .pb.go file.

.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 command: used to generate pb.go, grpc.go files.
protoc-go-inject-tag command: used to rewrite the tag in the pb.go file, so that json: “xxx” form:”xxx” and other tags can be added for operation.

For example, in the following proto file, if there is no protoc-go-inject-tag, then the NickName we generate is uppercase, that is, the accepted parameters are uppercase, but we generally use lowercase to accept parameters.

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;
}

Of course, in addition to this solution, we can write NickName as nickname lowercase, it is also possible, protoc will automatically change nickname into Nickname at the code level, the code level can still be called, and accepting parameters is also possible is lowercase. for example like this

message UserRequest{<!-- -->
  string nick_name=1;
  string user_name=2;
  string password=3;
  string password_confirm=4;
}

It depends on personal preference…

2.2.2 Quick start of the environment

Quick Start Environment

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

Here, the environment will be quickly started according to the compose file.


When creating, the database has already been created, so we only need to go to the cmd folder under each module to start the microservice, for example, go to app/user/cmd, start this main.go to start the user module up.

Close the environment quickly

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

The above is the important update in the grpc refactoring process. You can clone the project, run it yourself, and switch the v1 and v2 branches to feel the changes this time.