官方demo
前期准备
- 确保
GOPATH环境变量已经被正确地定义(例如export GOPATH=~/go)并且将$GOPATH/bin添加到PATH环境变量之中(例如export PATH=$GOPATH/bin:$PATH);请勿将GOPATH设置为当前用户没有读写权限的目录 - 安装 kitex:
go install github.com/cloudwego/kitex/tool/cmd/kitex@latest - 安装 thriftgo:
go install github.com/cloudwego/thriftgo@latest
示例代码
方式一:直接启动
-
进入示例仓库的
hello目录cd kitex-examples/hello -
运行 server
go run . -
运行 client
另起一个终端后,
go run ./client
运行结果:
server:
client:
增加一个新的方法
打开 hello.thrift,现在让我们为新方法分别定义一个新的请求和响应,AddRequest 和 AddResponse,并在 service Hello 中增加 add 方法:
namespace go api
struct Request {
1: string message
}
struct Response {
1: string message
}
struct AddRequest {
1: i64 first
2: i64 second
}
struct AddResponse {
1: i64 sum
}
service Hello {
Response echo(1: Request req)
AddResponse add(1: AddRequest req)
}
重新生成代码
运行如下命令后,kitex 工具将根据 hello.thrift 更新代码文件。
kitex -service a.b.c hello.thrift
# 若当前目录不在 $GOPATH/src 下,需要加上 -module 参数,一般为 go.mod 下的名字
kitex -module "your_module_name" -service a.b.c hello.thrift
执行完上述命令后,kitex 工具将更新下述文件
- 更新
./handler.go,在里面增加一个Add方法的基本实现 - 更新
./kitex_gen,里面有框架运行所必须的代码文件
更新服务端处理逻辑
上述步骤完成后,./handler.go 中会自动补全一个 Add 方法的基本实现,类似如下代码:
// Add implements the HelloImpl interface.
func (s *HelloImpl) Add(ctx context.Context, req *api.AddRequest) (resp *api.AddResponse, err error) {
// TODO: Your code here...
return
}
让我们在里面增加我们所需要的逻辑,类似如下代码:
// Add implements the HelloImpl interface.
func (s *HelloImpl) Add(ctx context.Context, req *api.AddRequest) (resp *api.AddResponse, err error) {
// TODO: Your code here...
resp = &api.AddResponse{Sum: req.First + req.Second}
return
}
增加客户端调用
服务端已经有了 Add 方法的处理,现在让我们在客户端增加对 Add 方法的调用。
在 ./client/main.go 中你会看到类似如下的 for 循环:
for {
req := &api.Request{Message: "my request"}
resp, err := client.Echo(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println(resp)
time.Sleep(time.Second)
}
现在让我们在里面增加 Add 方法的调用:
for {
req := &api.Request{Message: "my request"}
resp, err := client.Echo(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println(resp)
time.Sleep(time.Second)
addReq := &api.AddRequest{First: 512, Second: 512}
addResp, err := client.Add(context.Background(), addReq)
if err != nil {
log.Fatal(err)
}
log.Println(addResp)
time.Sleep(time.Second)
}
重新运行示例代码
关闭之前运行的客户端和服务端之后
-
运行 server
go run . -
运行 client
另起一个终端后,
go run ./client现在,你应该能看到客户端在调用
Add方法了。
运行结果:
server:
client:
微服务开发流程
完整微服务项目的目录结构
.
├── cmd
│ ├── api
│ │ ├── handlers
│ │ └── rpc
│ ├── note
│ │ ├── dal
│ │ │ └── db
│ │ ├── output
│ │ │ ├── bin
│ │ │ └── log
│ │ │ ├── app
│ │ │ └── rpc
│ │ ├── pack
│ │ ├── rpc
│ │ ├── script
│ │ └── service
│ └── user
│ ├── dal
│ │ └── db
│ ├── output
│ │ ├── bin
│ │ └── log
│ │ ├── app
│ │ └── rpc
│ ├── pack
│ ├── script
│ └── service
├── idl
├── images
├── kitex_gen
│ ├── notedemo
│ │ └── noteservice
│ └── userdemo
│ └── userservice
└── pkg
├── bound
├── configs
│ └── sql
├── constants
├── errno
├── middleware
└── tracer
目录介绍
| catalog | introduce |
|---|---|
| pkg/constants | constant |
| pkg/bound | customized bound handler |
| pkg/errno | customized error number |
| pkg/middleware | RPC middleware微服务通用中间件 |
| pkg/tracer | init jaeger链路追踪 |
| dal | 数据库操作 |
| pack | 数据装配 |
| service | 业务逻辑 |
1、单个微服务编写
1、创建目录
make dir project_name project_name/cmd project_name/idl project_name/kitex_gen project_name/pkg
2、编写IDL文件
syntax = "proto3";
package user;
option go_package = "userdemo";
message BaseResp {
int64 status_code = 1;
string status_message = 2;
int64 service_time = 3;
}
message User {
int64 user_id = 1;
string user_name = 2;
string avatar = 3;
}
message CreateUserRequest {
string user_name = 1;
string password = 2;
}
message CreateUserResponse {
BaseResp base_resp = 1;
}
message MGetUserRequest {
repeated int64 user_ids = 1;
}
message MGetUserResponse {
repeated User users = 1;
BaseResp base_resp = 2;
}
message CheckUserRequest{
string user_name = 1;
string password = 2;
}
message CheckUserResponse{
int64 user_id = 1;
BaseResp base_resp = 2;
}
service UserService {
rpc CreateUser (CreateUserRequest) returns (CreateUserResponse) {}
rpc MGetUser (MGetUserRequest) returns (MGetUserResponse) {}
rpc CheckUser (CheckUserRequest) returns (CheckUserResponse) {}
}
3、通过代码生成工具快速生成脚手架
#尽量使用thrift -module 包名 -service 服务名对应生成的目录名称 -gen-path string 对应生成代码存放位置 最后加上IDL文件
kitex -module easynote -service user -gen-path "../../kitex_gen/" ../../idl/user.thrift
4、添加单项微服务目录
#dal 保存数据库操作 pack 数据封装 service 具体的业务代码
mkdir dal pack service
5、编写公用包
该部分位于easynote/pkg/目录下,一般所有服务共同依赖的功能封装到该部分下,demo包含了以下几个部分:
-
bound:CPU检测
//cpu.go package bound import ( "context" "fmt" "net" "easynote/pkg/constants" "easynote/pkg/errno" "github.com/cloudwego/kitex/pkg/klog" "github.com/cloudwego/kitex/pkg/remote" "github.com/shirou/gopsutil/cpu" ) var _ remote.InboundHandler = &cpuLimitHandler{} type cpuLimitHandler struct{} func NewCpuLimitHandler() remote.InboundHandler { return &cpuLimitHandler{} } // OnActive implements the remote.InboundHandler interface. func (c *cpuLimitHandler) OnActive(ctx context.Context, conn net.Conn) (context.Context, error) { return ctx, nil } // OnRead implements the remote.InboundHandler interface. func (c *cpuLimitHandler) OnRead(ctx context.Context, conn net.Conn) (context.Context, error) { p := cpuPercent() klog.CtxInfof(ctx, "current cpu is %.2g", p) if p > constants.CPURateLimit { return ctx, errno.ServiceErr.WithMessage(fmt.Sprintf("cpu = %.2g", c)) } return ctx, nil } // OnInactive implements the remote.InboundHandler interface. func (c *cpuLimitHandler) OnInactive(ctx context.Context, conn net.Conn) context.Context { return ctx } // OnMessage implements the remote.InboundHandler interface. func (c *cpuLimitHandler) OnMessage(ctx context.Context, args, result remote.Message) (context.Context, error) { return ctx, nil } func cpuPercent() float64 { percent, _ := cpu.Percent(0, false) return percent[0] } -
constants:全局常量(数据库,链路追踪等等配置地址)
//constants.go package constants // 存放配置信息,可以通过Viper插件进行优化 const ( NoteTableName = "note" UserTableName = "user" SecretKey = "secret key" IdentityKey = "id" Total = "total" Notes = "notes" NoteID = "note_id" ApiServiceName = "demoapi" NoteServiceName = "demonote" UserServiceName = "demouser" MySQLDefaultDSN = "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local" EtcdAddress = "127.0.0.1:2379" CPURateLimit float64 = 80.0 DefaultLimit = 10 ) -
errno:全局错误封装
package errno import ( "errors" "fmt" ) const ( SuccessCode = 0 ServiceErrCode = 10001 ParamErrCode = 10002 UserAlreadyExistErrCode = 10003 AuthorizationFailedErrCode = 10004 ) // 通过空变量进行接口实现校验 var _ error = &ErrNo{} // 自定义错误返回类型 type ErrNo struct { ErrCode int64 ErrMsg string } // 继承error接口,实现自定义Error方法 func (e ErrNo) Error() string { return fmt.Sprintf("err_code=%d, err_msg=%s", e.ErrCode, e.ErrMsg) } func NewErrNo(code int64, msg string) ErrNo { return ErrNo{code, msg} } func (e ErrNo) WithMessage(msg string) ErrNo { e.ErrMsg = msg return e } // 常用封装错误类型 var ( Success = NewErrNo(SuccessCode, "Success") ServiceErr = NewErrNo(ServiceErrCode, "Service is unable to start successfully") ParamErr = NewErrNo(ParamErrCode, "Wrong Parameter has been given") UserAlreadyExistErr = NewErrNo(UserAlreadyExistErrCode, "User already exists") AuthorizationFailedErr = NewErrNo(AuthorizationFailedErrCode, "Authorization failed") ) // ConvertErr convert error to Errno func ConvertErr(err error) ErrNo { Err := ErrNo{} if errors.As(err, &Err) { return Err } s := ServiceErr s.ErrMsg = err.Error() return s } -
middleware:中间件
-
client客户端
//client.go package middleware import ( "context" "github.com/cloudwego/kitex/pkg/endpoint" "github.com/cloudwego/kitex/pkg/klog" "github.com/cloudwego/kitex/pkg/rpcinfo" ) var _ endpoint.Middleware = ClientMiddleware // ClientMiddleware client middleware print server address 、rpc timeout and connection timeout func ClientMiddleware(next endpoint.Endpoint) endpoint.Endpoint { return func(ctx context.Context, req, resp interface{}) (err error) { ri := rpcinfo.GetRPCInfo(ctx) // get server information klog.Infof("server address: %v, rpc timeout: %v, readwrite timeout: %v\n", ri.To().Address(), ri.Config().RPCTimeout(), ri.Config().ConnectTimeout()) if err = next(ctx, req, resp); err != nil { return err } return nil } } -
common
//common.go package middleware import ( "context" "github.com/cloudwego/kitex/pkg/endpoint" "github.com/cloudwego/kitex/pkg/klog" "github.com/cloudwego/kitex/pkg/rpcinfo" ) var _ endpoint.Middleware = CommonMiddleware // CommonMiddleware common middleware print some rpc info、real request and real response func CommonMiddleware(next endpoint.Endpoint) endpoint.Endpoint { return func(ctx context.Context, req, resp interface{}) (err error) { ri := rpcinfo.GetRPCInfo(ctx) // get real request klog.Infof("real request: %+v\n", req) // get remote service information klog.Infof("remote service name: %s, remote method: %s\n", ri.To().ServiceName(), ri.To().Method()) if err = next(ctx, req, resp); err != nil { return err } // get real response klog.Infof("real response: %+v\n", resp) return nil } } -
server服务端
//server.go package middleware import ( "context" "github.com/cloudwego/kitex/pkg/endpoint" "github.com/cloudwego/kitex/pkg/klog" "github.com/cloudwego/kitex/pkg/rpcinfo" ) var _ endpoint.Middleware = ServerMiddleware // ServerMiddleware server middleware print client address func ServerMiddleware(next endpoint.Endpoint) endpoint.Endpoint { return func(ctx context.Context, req, resp interface{}) (err error) { ri := rpcinfo.GetRPCInfo(ctx) // get client information klog.Infof("client address: %v\n", ri.From().Address()) if err = next(ctx, req, resp); err != nil { return err } return nil } }
-
-
tracer:链路追踪
//tracer.go package tracer import ( "fmt" "github.com/opentracing/opentracing-go" "github.com/uber/jaeger-client-go" jaegercfg "github.com/uber/jaeger-client-go/config" ) func InitJaeger(service string) { cfg, _ := jaegercfg.FromEnv() cfg.ServiceName = service tracer, _, err := cfg.NewTracer(jaegercfg.Logger(jaeger.StdLogger)) if err != nil { panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err)) } opentracing.SetGlobalTracer(tracer) return }
6、编写服务统一封装类
#cmd/service_name/pack
#一般编写两个封装类,一个返回值封装类,一个DAO到form(service)model对象封装类
//返回值封装resp.go
package pack
import (
"easynote/kitex_gen/user"
"easynote/pkg/errno"
"errors"
"time"
)
// 创建基本返回,通过封装统一错误
func BuildBaseResp(err error) *user.BaseResp {
if err == nil {
return baseResp(errno.Success)
}
e := errno.ErrNo{}
//如果生成错误已经是封装的错误类型,则直接返回
if errors.As(err, &e) {
//调用生成BaseResp的接口,传入错误信息
return baseResp(e)
}
//如果不是则直接调用默认服务错误类型配合对应的错误信息进行封装
s := errno.ServiceErr.WithMessage(err.Error())
return baseResp(s)
}
func baseResp(err errno.ErrNo) *user.BaseResp {
//返回三个信息,错误代码,错误信息,服务执行时间戳
return &user.BaseResp{StatusCode: err.ErrCode, StatusMessage: err.ErrMsg, ServiceTime: time.Now().Unix()}
}
//模式转换封装类user.go
package pack
import (
"easynote/cmd/user/dal/db"
"easynote/kitex_gen/user"
)
// pack 完成model 到 service user的转变(定义了两个结构体,类似gin中的form与model)
// User pack user info
func User(u *db.User) *user.User {
if u == nil {
return nil
}
return &user.User{UserId: int64(u.ID), UserName: u.UserName, Avatar: "test"}
}
// Users pack list of user info
func Users(us []*db.User) []*user.User {
users := make([]*user.User, 0)
for _, u := range us {
if user2 := User(u); user2 != nil {
users = append(users, user2)
}
}
return users
}
#全局错误(返回值处理)处理,这个是所有服务共同调用一个接口
//easynote/pkg/errno/errno.go
package errno
import (
"errors"
"fmt"
)
const (
SuccessCode = 0
ServiceErrCode = 10001
ParamErrCode = 10002
UserAlreadyExistErrCode = 10003
AuthorizationFailedErrCode = 10004
)
// 通过空变量进行接口实现校验
var _ error = &ErrNo{}
// 自定义错误返回类型
type ErrNo struct {
ErrCode int64
ErrMsg string
}
// 继承error接口,实现自定义Error方法
func (e ErrNo) Error() string {
return fmt.Sprintf("err_code=%d, err_msg=%s", e.ErrCode, e.ErrMsg)
}
func NewErrNo(code int64, msg string) ErrNo {
return ErrNo{code, msg}
}
func (e ErrNo) WithMessage(msg string) ErrNo {
e.ErrMsg = msg
return e
}
// 常用封装错误类型
var (
Success = NewErrNo(SuccessCode, "Success")
ServiceErr = NewErrNo(ServiceErrCode, "Service is unable to start successfully")
ParamErr = NewErrNo(ParamErrCode, "Wrong Parameter has been given")
UserAlreadyExistErr = NewErrNo(UserAlreadyExistErrCode, "User already exists")
AuthorizationFailedErr = NewErrNo(AuthorizationFailedErrCode, "Authorization failed")
)
// ConvertErr convert error to Errno
func ConvertErr(err error) ErrNo {
Err := ErrNo{}
if errors.As(err, &Err) {
return Err
}
s := ServiceErr
s.ErrMsg = err.Error()
return s
}
7、编写handle.go实现对应的方法
主要实现了一下三个方法:
- CreateUser
- MGetUser
- CheckUser
调用关系:RPC -> service -> dal -> db
这里实现的其实是RPC调用的最表层,类似于gin中的Controller层,所以一般我们可以在这里添加对应的参数校验功能(可以封装为client级别的中间件)
#handle.go
package main
import (
"context"
"easynote/cmd/user/pack"
"easynote/cmd/user/service"
"easynote/kitex_gen/user"
"easynote/pkg/errno"
)
// UserServiceImpl implements the last service interface defined in the IDL.
type UserServiceImpl struct{}
// CreateUser implements the UserServiceImpl interface.
func (s *UserServiceImpl) CreateUser(ctx context.Context, req *user.CreateUserRequest) (resp *user.CreateUserResponse, err error) {
//创建返回类型变量
resp = new(user.CreateUserResponse)
//参数校验不通过
if len(req.UserName) == 0 || len(req.Password) == 0 {
//调用封装数据方法,封装参数类型错误
resp.BaseResp = pack.BuildBaseResp(errno.ParamErr)
return resp, nil
}
//每次调用业务代码会新建一个ctx,调用业务代码
err = service.NewCreateuserService(ctx).CreateUser(req)
//错误处理
if err != nil {
resp.BaseResp = pack.BuildBaseResp(err)
return resp, nil
}
//封装统一返回
resp.BaseResp = pack.BuildBaseResp(errno.Success)
return resp, nil
}
// MGetUser implements the UserServiceImpl interface.
func (s *UserServiceImpl) MGetUser(ctx context.Context, req *user.MGetUserRequest) (resp *user.MGetUserResponse, err error) {
// TODO: Your code here...
//创建返回类型
resp = new(user.MGetUserResponse)
//参数校验
if len(req.UserIds) == 0 {
resp.BaseResp = pack.BuildBaseResp(errno.ParamErr)
return resp, nil
}
//调用业务操作
users, err := service.NewMgetUserService(ctx).MgetUser(req)
if err != nil {
resp.BaseResp = pack.BuildBaseResp(err)
return resp, nil
}
resp.BaseResp = pack.BuildBaseResp(errno.Success)
resp.Users = users
return resp, nil
}
// CheckUser implements the UserServiceImpl interface.
func (s *UserServiceImpl) CheckUser(ctx context.Context, req *user.CheckUserRequest) (resp *user.CheckUserResponse, err error) {
// TODO: Your code here...
//创建返回值
resp = new(user.CheckUserResponse)
//参数校验
if len(req.UserName) == 0 || len(req.Password) == 0 {
resp.BaseResp = pack.BuildBaseResp(errno.ParamErr)
return resp, nil
}
uid, err := service.NewCheckUserService(ctx).CheckUser(req)
if err != nil {
resp.BaseResp = pack.BuildBaseResp(err)
return resp, nil
}
resp.UserId = uid
resp.BaseResp = pack.BuildBaseResp(errno.Success)
return resp, nil
}
8、编写具体的业务逻辑
改代码应位于cmd/user/service目录下,每对映一个RPC服务,应该单独写一个服务类型,这样方便通过context传递上下文信息,这里我们举例一个服务。
package service
import (
"context"
"crypto/md5"
"easynote/cmd/user/dal/db"
"easynote/kitex_gen/user"
"easynote/pkg/errno"
"fmt"
"io"
)
//构造一个服务类,继承context接口
type CreateUserService struct {
ctx context.Context
}
// NewCreateUserService new CreateUserService,每次服务调用根据对应的context创建一个服务类型
func NewCreateuserService(ctx context.Context) *CreateUserService {
return &CreateUserService{ctx: ctx}
}
// 具体的service方法,复杂的业务逻辑可以在这里实现
func (s *CreateUserService) CreateUser(req *user.CreateUserRequest) error {
//先查询数据库是否存在用户
users, err := db.QueryUser(s.ctx, req.UserName)
if err != nil {
return err
}
if len(users) != 0 {
return errno.UserAlreadyExistErr
}
h := md5.New()
if _, err = io.WriteString(h, req.Password); err != nil {
return err
}
//经过MD5加密,但是不能防止字典攻击
passWord := fmt.Sprint("%x", h.Sum(nil))
return db.CreateUser(s.ctx, []*db.User{{
UserName: req.UserName,
Password: passWord,
}})
}
9、编写对应的dal操作
到目前为止,我们已经实现对了对RPC响应的部分,接下来的部分与RPC完全解耦合,与正常的web开发相同的DAO操作,你可以按照你自己喜欢的方式完成DAO层的开发,但是这里为了配合规范,我们采用官方demo的构建方式。
#此处是我们的目录规范
├── dal
├── db
│ ├── init.go
│ └── user.go
└── init.go
1、编写封装启动类(规范)
//init.go, 此处只是为了方便调用封装了内部的启动类
package dal
import (
"easynote/cmd/user/dal/db"
)
func Init() {
db.Init() // mysql init
}
2、编写启动类
//db/init.go
package db
import (
"easynote/pkg/constants"
"gorm.io/driver/mysql"
"gorm.io/gorm"
gormopentracing "gorm.io/plugin/opentracing"
)
var DB *gorm.DB
// Init init DB
func Init() {
var err error
DB, err = gorm.Open(mysql.Open(constants.MySQLDefaultDSN),
&gorm.Config{
PrepareStmt: true,
SkipDefaultTransaction: true,
},
)
if err != nil {
panic(err)
}
//添加了对应的链路追踪
if err = DB.Use(gormopentracing.New()); err != nil {
panic(err)
}
}
3、编写具体的DAL操作
正常情况我们的DAO开发会有对应的一个Model包来保存对应的实体类,但是由于目前学习到的微服务主要聚焦于单实体类->对应一个service,所以定义model与对应的DAO操作我们放到一个类中编写。
//dal/user.go
// model
type User struct {
gorm.Model
UserName string `json:"user_name"`
Password string `json:"password"`
}
//存储正确的表名,gorm会根据对应的实体类变复数然后查询该表
func (u *User) TableName() string {
return constants.UserTableName
}
// MGetUsers multiple get list of user info
func MGetUsers(ctx context.Context, userIDs []int64) ([]*User, error) {
res := make([]*User, 0)
if len(userIDs) == 0 {
return res, nil
}
//WithContext方法可以创建一个新的DBsession
if err := DB.WithContext(ctx).Where("id in ?", userIDs).Find(&res).Error; err != nil {
return nil, err
}
return res, nil
}
// CreateUser create user info
func CreateUser(ctx context.Context, users []*User) error {
//返回DB封装的错误类型
return DB.WithContext(ctx).Create(users).Error
}
// QueryUser query list of user info
func QueryUser(ctx context.Context, userName string) ([]*User, error) {
res := make([]*User, 0)
if err := DB.WithContext(ctx).Where("user_name = ?", userName).Find(&res).Error; err != nil {
return nil, err
}
return res, nil
}
10、Server启动类添加配置
package main
import (
"easynote/cmd/user/dal"
user "easynote/kitex_gen/user/userservice"
"easynote/pkg/bound"
"easynote/pkg/constants"
"easynote/pkg/middleware"
tracer2 "easynote/pkg/tracer"
"github.com/cloudwego/kitex/pkg/klog"
"github.com/cloudwego/kitex/pkg/limit"
"github.com/cloudwego/kitex/pkg/rpcinfo"
"github.com/cloudwego/kitex/server"
etcd "github.com/kitex-contrib/registry-etcd"
trace "github.com/kitex-contrib/tracer-opentracing"
"net"
)
// 辅助启动类
func Init() {
tracer2.InitJaeger(constants.UserServiceName)
dal.Init()
}
func main() {
r, err := etcd.NewEtcdRegistry([]string{constants.EtcdAddress})
if err != nil {
panic(err)
}
addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:18889")
if err != nil {
panic(err)
}
Init()
svr := user.NewServer(new(UserServiceImpl),
server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: constants.UserServiceName}), // 注册服务名称
server.WithMiddleware(middleware.CommonMiddleware), // 注册中间件
server.WithMiddleware(middleware.ServerMiddleware),
server.WithServiceAddr(addr), // 绑定服务地址
server.WithLimit(&limit.Option{MaxConnections: 1000, MaxQPS: 100}), // 连接配置
server.WithMuxTransport(), // 多路复用
server.WithSuite(trace.NewDefaultServerSuite()), // 链路追踪
server.WithBoundHandler(bound.NewCpuLimitHandler()), // CPU监测
server.WithRegistry(r), // 服务注册
)
err = svr.Run()
if err != nil {
klog.Fatal(err)
}
}
到此为止,我们对应的一个服务的Server端就已经编写完成,那么微服务还有一个重要的部分就是门户,也就是我们俗称的gateway,这里我们尝试学习和改造Kitex给我们提供的hertz版本,采用更为熟悉的GinHttp框架作为我们gateway的开发框架。