掘金 后端 ( ) • 2024-04-19 16:59

基于go-zero的goctl二开,gin项目代码生成器

代码地址:https://gitee.com/dn-jinmin/goctl-gin

gin框架很轻量以构建http服务为主,但gin本身是没有相关的项目代码生成工具,在业界中也有相关的解决方案

常规的项目中大多以如下目录结构设计

- project
  - internal
    - handler -- 或者 controller
    - logic   -- 或者 service
    - repo    -- 或者 models
  - pkg       -- 或者 util 或 common

整体上大同小异其目的就是划分模块然后完成功能,在常规工作中主要的重复工作为

  1. 创建控制器
  2. 注册控制器至路由处理中
  3. 对新增方法注册路由
  4. 创建logic业务处理对象
  5. 在控制器中引入 主要是上面的几个环节相对而言重复较多,因此基于这个问题我自己就开展了一个代码生成工具,该工具是基于go-zero的goctl二开得来,因此语法仍然是使用go-zero的定义

1. 使用

整体的语法与go-zero的goctl是一致的因此可以参考这里 https://go-zero.dev/docs/tasks/cli/api-format

如下是一个文件

syntax = "v1"

info(
    title: doc title
)

type Request struct {
    Name string `path:"name,options=you|me"`
}

type Response struct {
    Message string `json:"message"`
}

@server(
    group: admin/v1
    logic: Admin,User
)
service Aapi {
    @server( // C3
        handler: GreetHandler
        logic: Admin.GreetHandler
    )
    get /greet/from/:name(Request) returns (Response)   // hello

    @handler NoResponseHandler  // C5
    get /greet/get(Request)
}

@server(
    group: user/v1
    logic: User
)
service User {
    @server(
        handler: UserHandler
        logic: User.UserHandler
    )
    get /user/from/:name(Request) returns (Response)   // hello

    @handler NoUserResponseHandler  // C5
    get /user/get(Request)
}
@server(
    group: user/v2
    logic: User
)
service User {
    @server( // C3
        handler: UserHandler2
        logic: User.UserHandler2
    )
    get /user/from2/:name(Request) returns (Response)   // hello

    @handler NoUserResponseHandler2  // C5
    get /user/get2(Request)
}

生成命令

goctl-gin api go -api ./greet.api -dir ./ -style go-zero

生成结构如下

- api
  - etc
    - user.yaml
  - internal
    - config
      - config.go
    - domain
      - domain.go
    - handler
      - greet
        - aapi.go
        - handler.go
        - user.go
    - logic
      - admin.go
      - user.go
    - svc
      - servicecontext.go
  - pkg
    - conf
    - httpx
  - main.go
  - greet.api

1.1 参数说明

info . serviceMod

info(
    title: doc title
    serviceMod: 1
)
  • serviceMod 是选择类型,参数值为 1 或者 0;

在handler中,即为控制器中代码结构是如下结构

type User struct {
   svcCtx *svc.ServiceContext
   user   logic.User
}

func NewUser(svcCtx *svc.ServiceContext, user logic.User) *User {
   return &User{
      svcCtx: svcCtx,
      user:   user,
   }
}

func (h *User) InitRegister(engine *gin.Engine) {
   g0 := engine.Group("user/v1")
   g0.GET("/user/from/:name", h.UserHandler)
   g0.GET("/user/get", h.NoUserResponseHandler)

   g1 := engine.Group("user/v2")
   g1.GET("/user/from2/:name", h.UserHandler2)
   g1.GET("/user/get2", h.NoUserResponseHandler2)
}

考虑到在开发中还会有其他方法的新增顾设置0与1两种情况,

  • 0:不对handler中的控制器对象处理
  • 1:根据.api文件新增方法,自动将方法追加至文件末端,以及自动在struct中增加对新增Logic的引用,并且重写InitRegister;注意还需要自己根据情况完善NewUser中的内容

其中handler/module/handler.go会直接重写加载最新的,无需开发者关注,自动根据api中定义重写。

server中的logic

@server(
    group: admin/v1
    logic: Admin,User
)

在@server中定义logic,用于设置当前的控制器结构体对象引用某一个logic,多个用“,”分割,可以在重复引用,在handler/module/handler.go中会依据上面的规则生成代码,主要影响与initHandler方法

func initHandler(cfg config.Config) []Handler {
   svc := svc.NewServiceContext(cfg)

   // new logics
   var (
      adminLogic = logic.NewAdmin(svc)
      userLogic  = logic.NewUser(svc)
   )

   // new handlers
   var (
      user = NewUser(svc, userLogic)
      aapi = NewAapi(svc, adminLogic, userLogic)
   )

   return []Handler{
      user,
      aapi,
   }
}

@server( // C3
    handler: UserHandler2
    logic: User.UserHandler2
)
get /user/from2/:name(Request) returns (Response)   // hello

在方法中的logic,主要作用是用于约定当前方法使用具体哪个logic中的方法,其效果如下;注意logic中的方法参数返回与路由定义是一致的。

func (h *User) UserHandler(ctx *gin.Context) {
   var req domain.Request
   if err := ctx.ShouldBind(&req); err != nil {
      httpx.FailWithErr(ctx, err)
      return
   }

   res, err := h.user.UserHandler(ctx, &req)
   if err != nil {
      httpx.FailWithErr(ctx, err)
   } else {
      httpx.OkWithData(ctx, res)
   }
}

关于module

module是依据文件名定义,因此一个文件中存在多个server

2. 关于如何基于go-zero的goctl扩展

在下面主要介绍如何基于go-zero的goctl扩展,整体的难度不是特别大;

1.png

命令从goctl.go入口开始根据命令的选择进入相应的包中处理,当前我们是api代码的生成,因此命令因是

goctl-gin api go -api ./greet.api -dir ./ -style go-zero

2.png

而在每个封装的代码生成目录中是如上结构的引用,主要选择的是核心

  • spec:定义的是解析后的字段
  • parser:是用于文件解析
  • xxgen: 带有gen的都是生成文件代码的

调度结构就是这样,在每个xxgen文件中一般会具有一个gen.go的文件其中GoCommand方法就是命令的入口,剩下的就可以自己根据代码结构来分析啦

func GoCommand(_ *cobra.Command, _ []string) error {

   apiFile := VarStringAPI
   dir := VarStringDir
   namingStyle := VarStringStyle
   home := VarStringHome
   remote := VarStringRemote
   branch := VarStringBranch
   if len(remote) > 0 {
      repo, _ := util.CloneIntoGitHome(remote, branch)
      if len(repo) > 0 {
         home = repo
      }
   }

   if len(home) > 0 {
      pathx.RegisterGoctlHome(home)
   }
   if len(apiFile) == 0 {
      return errors.New("missing -api")
   }
   if len(dir) == 0 {
      return errors.New("missing -dir")
   }

   return DoGenProject(apiFile, dir, namingStyle)
}