API 代码生成
基于内置的 gooze 命令行工具 go run main.go gen api ... 实现
建议
如果你把 Admin-Server 和 Customer-API 放在了同一个项目下时,建议分层
如 admin 目录是管理后台的 API,customer 目录是面向 C 端的 API
以下示例分了项目,admin 和 customer 是单独的项目
创建 api 描述文件
mkdir api
touch api/test1.api📘 API 描述文件格式说明(.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
}你必须要先知道的事情
api 的文件名将决定生成的
dto、handler、logic、router的文件名同时也决定生成的
LogicName比如你的文件名是
system_user那么生成的LogicName将是NewSystemUserLogic服务模块
service ***主要是用来区分路由组的一个模块就是一个路由组,一个 api 文件内可以包含多个模块,但
service + Group不能重复同时,这个服务名,也将决定生成的路由前缀
比如
service SystemUser生成的路由前缀将是/system/user
- 语法详解
✅ 基础结构体定义(type)
用于定义接口的请求参数、响应结构。
type开头的内容最终会生成对应的结构体,内容和 Gin 的保持一致请求参数最好以
Req结尾,响应参数最好以Resp结尾它的 tag 支持
名称 描述 json通用(序列化、 post请求参数等)form绑定 get请求的参数binding参数校验规则,规则的定义与 Gin的保持一致description这个主要用于 swagger文档生成时的描述
✅ 接口模块定义(service)
用于定义接口分组、请求方法、参数和返回结构体。
必须以 service 打头,会生成对应的接口、方法
每个接口方法需独占一行,不支持多方法同行定义
如果没有请求参数,可以省略 (),或使用 returns 直接结尾
如果没有返回值,使用 returns(不加结构体名)
Group Auth表示这个模块属于哪个GroupGroup的作用Group 是指这个模块所属的组,目前支持
Public、Auth、Token它会将路由分成不同的组,从而让不同的组使用不同的中间件
名称 描述 Public不使用任何中间件 Auth使用 Casbin权限校验和Jwttoken 中间件,适用于管理后台Token使用 Jwttoken 中间件
- 示例生成效果
生成的请求响应结构体代码如下:
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 生成的路由代码如下:
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 你会看到以下输出
- 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)所以最终的执行命令如下:
go run main.go gen api \
--config="./configs/config.yaml" \
--env=".env" \
--src="./api" \
--output="./internal" \
--log=false| 配置项 | 作用域 | 作用域 | 描述 | ||
|---|---|---|---|---|---|
--config | string | 全局参数 | ✅ | 无 | 你的配置文件路径 |
--env | string | 全局参数 | ❌ | 无 | 通过 env 文件,重写配置文件内的内容 |
--src | string | 特定参数 | ✅ | 无 | api 描述文件的路径 |
--output | string | 特定参数 | ✅ | 无 | 生成代码的保存路径 |
--log | bool | 特定参数 | ❌ | 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 中匿名导入自动生成的服务代码即可
package main
import (
_ "your_project_name/internal/bootstrap"
"github.com/soryetong/gooze-starter/gooze"
)
func main() {
gooze.Run()
}Server 内容说明
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()前执行任何你想执行的操作