Skip to content

API 代码生成

基于内置的 gooze 命令行工具 go run main.go gen api ... 实现


建议

如果你把 Admin-Server 和 Customer-API 放在了同一个项目下时,建议分层

如 admin 目录是管理后台的 API,customer 目录是面向 C 端的 API

以下示例分了项目,admin 和 customer 是单独的项目

创建 api 描述文件

bash
mkdir api

touch api/test1.api

📘 API 描述文件格式说明(.api)

  1. 文件结构示例
api
# 文件: api/test1.api

# 定义请求结构体
type TestReq {
    Page int64 `json:"page" form:"page" binding:"required" description:"页数"`
    PageSize int64 `json:"pageSize" form:"pageSize" binding:"required" description:"每页数量"`
}

# 定义响应结构体
type TestResp {
    Name string `json:"name"`
}

# 定义服务模块
service SystemTest1 Group Public { # 这一行的内容,同一个文件中,不能重复
    get test1 (TestReq) returns (TestResp)
    post test2 returns
}

service SystemTest2 Group Auth {
    get test1 (TestReq) returns (TestResp)
    post test2 returns
}

你必须要先知道的事情

  1. api 的文件名将决定生成的 dtohandlerlogicrouter 的文件名

    同时也决定生成的 LogicName

    比如你的文件名是 system_user 那么生成的 LogicName 将是 NewSystemUserLogic

  2. 服务模块 service *** 主要是用来区分路由组的

    一个模块就是一个路由组,一个 api 文件内可以包含多个模块,但 service + Group 不能重复

    同时,这个服务名,也将决定生成的路由前缀

    比如 service SystemUser 生成的路由前缀将是 /system/user


  1. 语法详解

✅ 基础结构体定义(type)

用于定义接口的请求参数、响应结构。

  • type 开头的内容最终会生成对应的结构体,内容和 Gin 的保持一致

  • 请求参数最好以 Req 结尾,响应参数最好以 Resp 结尾

  • 它的 tag 支持

    名称描述
    json通用(序列化、post 请求参数等)
    form绑定 get 请求的参数
    binding参数校验规则,规则的定义与 Gin 的保持一致
    description这个主要用于 swagger 文档生成时的描述

✅ 接口模块定义(service)

用于定义接口分组、请求方法、参数和返回结构体。

  • 必须以 service 打头,会生成对应的接口、方法

  • 每个接口方法需独占一行,不支持多方法同行定义

  • 如果没有请求参数,可以省略 (),或使用 returns 直接结尾

  • 如果没有返回值,使用 returns(不加结构体名)

  • Group Auth 表示这个模块属于哪个 Group

    Group 的作用

    Group 是指这个模块所属的组,目前支持 PublicAuthToken

    它会将路由分成不同的组,从而让不同的组使用不同的中间件

    名称描述
    Public不使用任何中间件
    Auth使用 Casbin 权限校验和 Jwt token 中间件,适用于管理后台
    Token使用 Jwt token 中间件

  1. 示例生成效果

生成的请求响应结构体代码如下:

go
type TestReq struct {
	Page     int64 `json:"page" form:"page" binding:"required" description:"页数" `
	PageSize int64 `json:"pageSize" form:"pageSize" binding:"required" description:"每页数量"`
}

type TestResp struct {
	Name string `json:"name"`
}

不同的 Group 生成的路由代码如下:

go
publicGroup := r.Group("/api/v1")
{
    // 健康监测
    publicGroup.GET("/health", func(c *gin.Context) {
        c.JSON(200, "ok")
    })

    // your_router
}

privateAuthGroup := r.Group("/api/v1")
privateAuthGroup.Use(gzmiddleware.Casbin()).Use(gzmiddleware.Jwt())
{
    // your_router
}

privateTokenGroup := r.Group("/api/v1")
privateTokenGroup.Use(gzmiddleware.Jwt())
{
    // your_router
}

执行命令生成

当你运行 go run main.go gen api 你会看到以下输出

bash
-   gen     Code generation entry point for gooze-starter.
            Available Subcommands: - api Generate Gin route & handler based on .api spec file

            --src Path to the .api description file (required)

            --output Output directory for generated handler files (required)

            --log Open request Log (default: false)

所以最终的执行命令如下:

bash
go run main.go gen api \
  --config="./configs/config.yaml" \
  --env=".env" \
  --src="./api" \
  --output="./internal" \
  --log=false
配置项作用域作用域描述
--configstring全局参数你的配置文件路径
--envstring全局参数通过 env 文件,重写配置文件内的内容
--srcstring特定参数api 描述文件的路径
--outputstring特定参数生成代码的保存路径
--logbool特定参数false是否需要请求、响应日志记录中间件

TIP

路径参数支持 绝对路径 和 相对路径

目录结构

🎉 那么当前你的项目结构应该可能是这样的:

├─ your_project_name
├── api/                      # API 描述文件(如 user.api)
│   └── user.api

├── configs/                  # 应用配置文件
│   └── config.yaml           # 主配置文件(可配合 .env 使用)

├── docs/                     # 文档入口
│   ├── swagger/              # Swagger 接口文档
│   │   └── user.yaml

├── internal/                 # 核心业务代码
│   ├── handler/              # 控制器层(接收请求,返回响应)
│   ├── dto/                  # 请求/响应的数据结构
│   ├── router/               # 路由定义
│   ├── service/              # 业务逻辑
│   └── bootstrap/            # 启动逻辑

├── go.mod                    # Go 模块定义
├── go.sum                    # Go 依赖校验文件
└── main.go                   # 入口文件

TIP

所以这就是 gooze-starter 的好处之一,它会帮你快速生成那些无关紧要的代码

而且只会强制 internal 目录下的文件结构,不会干涉你其他目录的定义

你也不用纠结 「当新开一个项目,那些文件该放哪里,怎么放才能避免循环依赖 import cycle not allowed」这些问题了

加载 Server

当我们代码自动生成后,启动一个 HTTP 服务,只需要一行代码即可

TIP

服务启动代码位于:internal/bootstrap/**_Server.go

main.go 中匿名导入自动生成的服务代码即可

go
package main

import (
    _ "your_project_name/internal/bootstrap"

	"github.com/soryetong/gooze-starter/gooze"
)

func main() {
	gooze.Run()
}

Server 内容说明

go
package bootstrap

import (
	"github.com/soryetong/gooze-starter/gooze"
	"github.com/soryetong/gooze-starter/modules/httpmodule"
	"github.com/soryetong/gooze-starter/pkg/gzutil"

	"your_project_name/internal/router" // 注意修改
)

func init() {
	gooze.RegisterService(&HttpServer{})
}

type HttpServer struct {
	*gooze.IServer

	httpModule httpmodule.IHttp
}

func (self *HttpServer) OnStart() (err error) {
	// 添加回调函数
	self.httpModule.OnStop(self.exitCallback())

	self.httpModule.Init(self, gooze.Config.App.Addr, gooze.Config.App.Timeout, router.InitRouter())
	err = self.httpModule.Start()

	return
}

// TODO 添加回调函数, 无逻辑可直接删除这个方法
func (self *HttpServer) exitCallback() *gzutil.OrderlyMap {
	callback := gzutil.NewOrderlyMap()
	callback.Append("exit", func() {
		gooze.Log.Info("这是程序退出后的回调函数, 执行你想要执行的逻辑, 无逻辑可以直接删除这段代码")
	})

	return callback
}
  • main.go 中匿名导入这个文件,就是为了执行 init() 进行服务注册

  • 每一个 server 必须 “继承“ gooze.IServer 并且实现 OnStart() 方法

  • exitCallback() 函数是为了在服务停机时执行一些操作,比如:更新数据、清除缓存等等,你没有这些操作可以不要

    • 但是要注意,gzutil.OrderlyMap 是一个有序的 map,严格按照 Append 的顺序执行, 先进先出,同名会被覆盖
  • 你可以在 self.httpModule.Start() 前执行任何你想执行的操作

基于 MIT 许可发布