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
表示这个模块属于哪个Group
Group
的作用Group 是指这个模块所属的组,目前支持
Public
、Auth
、Token
它会将路由分成不同的组,从而让不同的组使用不同的中间件
名称 描述 Public
不使用任何中间件 Auth
使用 Casbin
权限校验和Jwt
token 中间件,适用于管理后台Token
使用 Jwt
token 中间件
- 示例生成效果
生成的请求响应结构体代码如下:
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()
前执行任何你想执行的操作