Qi Framework
基于 Gin 的轻量级 Go Web 框架
统一响应 · 自动绑定 · 泛型路由 · 优雅关机
$ go get github.com/tokmz/qi@latest
特性概览
基于 Gin
继承 Gin 的高性能和稳定性,无缝兼容 Gin 生态
统一响应
标准化 JSON 响应格式,自动注入 TraceID
自动绑定
根据 Content-Type 和 HTTP 方法自动绑定请求参数
泛型路由
Go 泛型简化路由处理,减少 70% 样板代码
错误处理
统一错误码和 HTTP 状态码映射
链路追踪
内置 TraceID 支持,OpenTelemetry 集成
Options 模式
灵活的函数选项配置方式
优雅关机
内置信号监听和生命周期回调
封装设计
Context 包装器提供清晰的 API 边界
内置 Recovery
默认启用 panic 恢复,防止服务崩溃
国际化
内置 i18n 支持,JSON 翻译、变量替换、复数形式
丰富中间件
CORS、限流、Gzip、超时控制、链路追踪
快速开始
基础用法
package main
import "github.com/tokmz/qi"
func main() {
// 创建 Engine(New() 默认包含 Recovery,Default() 额外添加 Logger)
engine := qi.Default()
r := engine.Router()
// 基础路由
r.GET("/ping", func(c *qi.Context) {
c.Success("pong")
})
// 手动绑定参数(绑定失败时自动响应错误)
r.POST("/user", func(c *qi.Context) {
var req CreateUserReq
if err := c.BindJSON(&req); err != nil {
return // 绑定失败已自动响应错误,直接 return 即可
}
c.Success(&UserResp{ID: 1, Name: req.Name})
})
// 启动服务器(支持优雅关机)
engine.Run(":8080")
}
使用 Options 配置
import (
"time"
"github.com/tokmz/qi"
"github.com/gin-gonic/gin"
)
func main() {
engine := qi.New(
qi.WithMode(gin.ReleaseMode),
qi.WithAddr(":8080"),
qi.WithReadTimeout(15 * time.Second),
qi.WithWriteTimeout(15 * time.Second),
qi.WithShutdownTimeout(30 * time.Second),
qi.WithBeforeShutdown(func() {
log.Println("清理资源...")
}),
qi.WithAfterShutdown(func() {
log.Println("关机完成")
}),
qi.WithTrustedProxies("127.0.0.1"),
)
r := engine.Router()
r.GET("/ping", func(c *qi.Context) {
c.Success("pong")
})
if err := engine.Run(); err != nil {
log.Fatal(err)
}
}
泛型路由
Qi 利用 Go 泛型提供三种路由函数,自动处理参数绑定和响应,减少约 70% 的样板代码。
Handle[Req, Resp] — 有请求有响应
qi.Handle[CreateUserReq, UserResp](r.POST, "/user",
func(c *qi.Context, req *CreateUserReq) (*UserResp, error) {
// req 已自动绑定,响应自动处理
return &UserResp{ID: 1, Name: req.Name}, nil
})
Handle0[Req] — 有请求无响应
qi.Handle0[DeleteUserReq](r.DELETE, "/user/:id",
func(c *qi.Context, req *DeleteUserReq) error {
// 自动绑定 URI 参数
return deleteUser(req.ID)
})
HandleOnly[Resp] — 无请求有响应
qi.HandleOnly[InfoResp](r.GET, "/info",
func(c *qi.Context) (*InfoResp, error) {
return &InfoResp{Version: "1.0.0"}, nil
})
泛型路由 + 中间件
qi.Handle[CreateUserReq, UserResp](r.POST, "/admin/user",
createUserHandler,
authMiddleware, // 第一个中间件
adminMiddleware, // 第二个中间件
)
路由与中间件
路由组
engine := qi.Default()
r := engine.Router()
// 路由组中间件
v1 := r.Group("/api/v1")
v1.Use(authMiddleware)
qi.Handle[LoginReq, TokenResp](v1.POST, "/login", loginHandler)
中间件注册方式
// 1. 全局中间件
engine.Use(traceMiddleware)
// 2. 路由组中间件
v1 := r.Group("/api/v1")
v1.Use(authMiddleware)
// 3. 单个路由中间件(不需要路由组)
r.GET("/admin/dashboard", dashboardHandler, authMiddleware, adminMiddleware)
// 4. 泛型路由中间件
qi.Handle[Req, Resp](r.POST, "/user", handler, middleware1, middleware2)
执行顺序
全局中间件 (engine.Use)
↓
路由组中间件 (group.Use)
↓
路由中间件 (variadic args)
↓
Handler 函数
自定义中间件示例
func traceMiddleware(c *qi.Context) {
traceID := c.GetHeader("X-Trace-ID")
if traceID == "" {
traceID = generateTraceID()
}
qi.SetContextTraceID(c, traceID)
c.Header("X-Trace-ID", traceID)
c.Next()
}
自动绑定
Qi 根据 HTTP 方法和 Content-Type 自动选择绑定策略:
| HTTP 方法 | 绑定策略 |
|---|---|
GET / DELETE | ShouldBindQuery + ShouldBindUri |
POST / PUT / PATCH | ShouldBind(根据 Content-Type 自动选择)+ ShouldBindUri |
| 其他方法 | ShouldBind(自动检测) |
Content-Type 映射:
| Content-Type | 绑定方式 |
|---|---|
application/json | JSON |
application/xml | XML |
application/x-www-form-urlencoded | Form |
multipart/form-data | Multipart Form |
绑定方法(自动响应错误)
所有 Bind* 方法在失败时自动响应 400 错误,只需判断 err != nil 并 return:
// BindJSON - 绑定 JSON 请求体
if err := c.BindJSON(&req); err != nil {
return // 已自动响应 400 错误
}
// BindQuery - 绑定 URL 查询参数
if err := c.BindQuery(&req); err != nil {
return
}
// BindURI - 绑定路径参数
if err := c.BindURI(&req); err != nil {
return
}
// BindHeader - 绑定请求头
if err := c.BindHeader(&req); err != nil {
return
}
// Bind - 根据 Content-Type 自动选择
if err := c.Bind(&req); err != nil {
return
}
请求结构体示例
// JSON 请求
type CreateUserReq struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
}
// Form 请求
type LoginReq struct {
Username string `form:"username" binding:"required"`
Password string `form:"password" binding:"required"`
}
// 文件上传
type UploadReq struct {
File *multipart.FileHeader `form:"file" binding:"required"`
}
// URI 参数
type GetUserReq struct {
ID int64 `uri:"id" binding:"required,min=1"`
}
响应格式
标准响应
{
"code": 200,
"data": {...},
"message": "success",
"trace_id": "xxx"
}
分页响应
{
"code": 200,
"data": {
"list": [...],
"total": 100
},
"message": "success"
}
响应方法
| 方法 | 说明 |
|---|---|
c.Success(data) | 成功响应 |
c.SuccessWithMessage(data, msg) | 成功响应(自定义消息) |
c.Nil() | 成功响应(无数据) |
c.Fail(code, message) | 失败响应 |
c.RespondError(err) | 错误响应(自动映射 HTTP 状态码) |
c.Page(list, total) | 分页响应 |
分页用法
// 方式 1:使用 Context.Page(推荐)
r.GET("/users", func(c *qi.Context) {
users := []User{...}
c.Page(users, 100)
})
// 方式 2:使用 NewPageResp
resp := qi.NewPageResp(users, 100)
c.Success(resp)
// 方式 3:使用 PageData
resp := qi.PageData(users, 100)
c.JSON(200, resp)
错误处理
错误类型
type Error struct {
Code int // 业务错误码 (1000-9999)
HttpCode int // HTTP 状态码
Message string // 错误消息
Err error // 原始错误
}
预定义错误
| 错误 | 业务码 | HTTP 状态码 | 说明 |
|---|---|---|---|
ErrServer | 1000 | 500 | 服务器错误 |
ErrBadRequest | 1001 | 400 | 请求参数错误 |
ErrUnauthorized | 1002 | 401 | 未授权 |
ErrForbidden | 1003 | 403 | 禁止访问 |
ErrNotFound | 1004 | 404 | 资源不存在 |
使用方式
import "github.com/tokmz/qi/pkg/errors"
// 使用预定义错误
return nil, errors.ErrBadRequest.WithMessage("用户名不能为空")
// 自定义错误
return nil, errors.New(2001, 403, "禁止访问", nil)
// 包装原始错误
return nil, errors.ErrServer.WithError(err)
提示:Handler 返回 error 后,
RespondError() 会自动检查是否为 *errors.Error 类型,并映射到对应的 HTTP 状态码。国际化 (i18n)
Qi 内置国际化支持,通过 WithI18n 配置即可启用。
创建翻译文件
// locales/zh-CN.json
{
"hello": "你好 {{.Name}}",
"user": {
"login": "登录",
"logout": "退出登录"
}
}
// locales/en-US.json
{
"hello": "Hello {{.Name}}",
"user": {
"login": "Login",
"logout": "Logout"
}
}
启用 i18n
import "github.com/tokmz/qi/pkg/i18n"
engine := qi.New(
qi.WithI18n(&i18n.Config{
Dir: "./locales",
DefaultLanguage: "zh-CN",
Languages: []string{"zh-CN", "en-US"},
}),
)
框架自动初始化翻译器并注册语言检测中间件。
语言检测优先级:Query(lang) > X-Language Header > Accept-Language Header > 默认语言
在路由中使用
r.GET("/hello", func(c *qi.Context) {
msg := c.T("hello", "Name", "Alice")
c.Success(msg)
})
// 泛型路由
qi.Handle[HelloReq, HelloResp](r.POST, "/hello",
func(c *qi.Context, req *HelloReq) (*HelloResp, error) {
msg := c.T("hello", "Name", req.Name)
return &HelloResp{Message: msg}, nil
})
复数形式
// 翻译文件: {"item_one": "{{.Count}} item", "item_other": "{{.Count}} items"}
c.Tn("item_one", "item_other", 1) // "1 item"
c.Tn("item_one", "item_other", 5) // "5 items"
获取翻译器实例
t := engine.Translator()
t.Preload("ja-JP")
t.HasKey("hello")
语言回退:当请求的语言中找不到翻译键时,自动回退到默认语言。如果默认语言也找不到,返回 key 本身。
优雅关机
Qi 内置优雅关机支持,自动监听 SIGINT 和 SIGTERM 信号。
engine := qi.New(
qi.WithShutdownTimeout(30 * time.Second),
qi.WithBeforeShutdown(func() {
log.Println("关闭数据库连接...")
db.Close()
}),
qi.WithAfterShutdown(func() {
log.Println("清理完成")
}),
)
// Run 会阻塞直到收到关机信号
if err := engine.Run(":8080"); err != nil {
log.Fatal(err)
}
手动关机
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := engine.Shutdown(ctx); err != nil {
log.Printf("关机失败: %v", err)
}
中间件
核心中间件(qi 包内置)
| 中间件 | 说明 | 启用方式 |
|---|---|---|
Recovery | panic 恢复 | qi.New() 默认启用 |
Logger | 请求日志 | qi.Default() 默认启用 |
扩展中间件(middleware 包)
| 中间件 | 说明 |
|---|---|
middleware.Tracing() | OpenTelemetry 链路追踪 |
middleware.CORS() | 跨域资源共享 |
middleware.RateLimiter() | 令牌桶限流 |
middleware.Timeout() | 请求超时控制 |
middleware.Gzip() | 响应压缩 |
注意:i18n 中间件已内置到框架中,通过
qi.WithI18n() 配置即可自动注册,无需手动添加。推荐注册顺序
e := qi.Default() // 内置 Recovery + Logger
// 1. 链路追踪(最先,创建根 Span + 生成 TraceID)
e.Use(middleware.Tracing())
// 2. CORS(在业务逻辑之前处理跨域预检)
e.Use(middleware.CORS())
// 3. 限流(在业务处理之前拦截超限请求)
e.Use(middleware.RateLimiter())
// 4. 超时控制
e.Use(middleware.Timeout())
// 5. Gzip 压缩
e.Use(middleware.Gzip())
// i18n 中间件通过 WithI18n 配置自动注册
配置选项
完整 Options 列表
| Option | 说明 | 默认值 |
|---|---|---|
WithMode(mode) | 运行模式 | gin.DebugMode |
WithAddr(addr) | 监听地址 | :8080 |
WithReadTimeout(d) | 读取超时 | 10s |
WithWriteTimeout(d) | 写入超时 | 10s |
WithIdleTimeout(d) | 空闲超时 | 60s |
WithMaxHeaderBytes(n) | 最大请求头 | 1MB |
WithShutdownTimeout(d) | 关机超时 | 10s |
WithBeforeShutdown(fn) | 关机前回调 | nil |
WithAfterShutdown(fn) | 关机后回调 | nil |
WithTrustedProxies(p...) | 信任的代理 | nil |
WithMaxMultipartMemory(n) | Multipart 内存 | 32MB |
WithI18n(cfg) | 国际化配置 | nil(不启用) |
完整配置示例
engine := qi.New(
qi.WithMode(gin.ReleaseMode),
qi.WithAddr(":8080"),
qi.WithReadTimeout(10 * time.Second),
qi.WithWriteTimeout(10 * time.Second),
qi.WithIdleTimeout(60 * time.Second),
qi.WithMaxHeaderBytes(1 << 20),
qi.WithShutdownTimeout(10 * time.Second),
qi.WithBeforeShutdown(func() { /* cleanup */ }),
qi.WithAfterShutdown(func() { /* finalize */ }),
qi.WithTrustedProxies("127.0.0.1"),
qi.WithMaxMultipartMemory(32 << 20),
qi.WithI18n(&i18n.Config{
Dir: "./locales",
DefaultLanguage: "zh-CN",
Languages: []string{"zh-CN", "en-US"},
}),
)
API 参考
Engine API
创建 Engine
// New 创建一个新的 Engine 实例(包含 Recovery 中间件)
func New(opts ...Option) *Engine
// Default 创建带有 Logger + Recovery 中间件的 Engine
func Default(opts ...Option) *Engine
Engine 方法
| 方法 | 说明 |
|---|---|
Use(middlewares ...HandlerFunc) | 注册全局中间件 |
Group(path, middlewares...) *RouterGroup | 创建路由组 |
Router() *RouterGroup | 返回根路由组 |
Translator() i18n.Translator | 返回 i18n 翻译器(未启用返回 nil) |
Run(addr ...string) error | 启动 HTTP 服务器(支持优雅关机) |
RunTLS(addr, cert, key) error | 启动 HTTPS 服务器 |
Shutdown(ctx) error | 手动关闭服务器 |
RouterGroup API
路由方法
| 方法 | 说明 |
|---|---|
GET(path, handler, mw...) | 注册 GET 路由 |
POST(path, handler, mw...) | 注册 POST 路由 |
PUT(path, handler, mw...) | 注册 PUT 路由 |
DELETE(path, handler, mw...) | 注册 DELETE 路由 |
PATCH(path, handler, mw...) | 注册 PATCH 路由 |
HEAD(path, handler, mw...) | 注册 HEAD 路由 |
OPTIONS(path, handler, mw...) | 注册 OPTIONS 路由 |
Any(path, handler, mw...) | 注册所有 HTTP 方法 |
Group(path, mw...) *RouterGroup | 创建子路由组 |
Use(middlewares...) | 注册中间件到路由组 |
静态文件
| 方法 | 说明 |
|---|---|
Static(path, root) | 静态文件目录服务 |
StaticFile(path, filepath) | 单个静态文件服务 |
StaticFS(path, fs) | 静态文件系统服务 |
泛型路由函数
// Handle 有请求参数,有响应数据
func Handle[Req, Resp any](register RouteRegister, path string,
handler func(*Context, *Req) (*Resp, error), middlewares ...HandlerFunc)
// Handle0 有请求参数,无响应数据
func Handle0[Req any](register RouteRegister, path string,
handler func(*Context, *Req) error, middlewares ...HandlerFunc)
// HandleOnly 无请求参数,有响应数据
func HandleOnly[Resp any](register RouteRegister, path string,
handler func(*Context) (*Resp, error), middlewares ...HandlerFunc)
Context API
请求信息
| 方法 | 说明 |
|---|---|
Request() *http.Request | 底层 Request |
Writer() gin.ResponseWriter | 底层 ResponseWriter |
Param(key) string | 路径参数 |
FullPath() string | 路由模板路径 |
Query(key) string | URL 查询参数 |
DefaultQuery(key, def) string | 查询参数(带默认值) |
GetQuery(key) (string, bool) | 查询参数(返回是否存在) |
PostForm(key) string | POST 表单参数 |
DefaultPostForm(key, def) string | 表单参数(带默认值) |
GetPostForm(key) (string, bool) | 表单参数(返回是否存在) |
ClientIP() string | 客户端 IP |
ContentType() string | Content-Type |
GetHeader(key) string | 请求头 |
参数绑定(自动响应错误)
| 方法 | 说明 |
|---|---|
Bind(obj) error | 自动选择绑定方式 |
BindJSON(obj) error | 绑定 JSON |
BindQuery(obj) error | 绑定查询参数 |
BindURI(obj) error | 绑定路径参数 |
BindHeader(obj) error | 绑定请求头 |
参数绑定(不自动响应)
| 方法 | 说明 |
|---|---|
ShouldBind(obj) error | 自动选择绑定方式 |
ShouldBindJSON(obj) error | 绑定 JSON |
ShouldBindQuery(obj) error | 绑定查询参数 |
ShouldBindUri(obj) error | 绑定路径参数 |
ShouldBindHeader(obj) error | 绑定请求头 |
响应方法
| 方法 | 说明 |
|---|---|
Success(data) | 成功响应 |
SuccessWithMessage(data, msg) | 成功响应(自定义消息) |
Nil() | 成功响应(无数据) |
Fail(code, message) | 失败响应 |
RespondError(err) | 错误响应 |
Page(list, total) | 分页响应 |
JSON(code, obj) | 发送 JSON |
Header(key, value) | 设置响应头 |
国际化
| 方法 | 说明 |
|---|---|
T(key, args...) string | 获取翻译(支持变量替换) |
Tn(key, plural, n, args...) string | 获取翻译(支持复数形式) |
上下文操作
| 方法 | 说明 |
|---|---|
Set(key, value) | 设置键值对 |
Get(key) (any, bool) | 获取键值对 |
GetString(key) string | 获取字符串值 |
GetInt(key) int | 获取整数值 |
GetInt64(key) int64 | 获取 int64 值 |
GetBool(key) bool | 获取布尔值 |
GetFloat64(key) float64 | 获取 float64 值 |
中间件控制
| 方法 | 说明 |
|---|---|
Next() | 执行下一个中间件 |
Abort() | 中止请求处理 |
AbortWithStatus(code) | 中止并设置状态码 |
AbortWithStatusJSON(code, obj) | 中止并返回 JSON |
IsAborted() bool | 检查是否已中止 |
Context 传递
| 方法 | 说明 |
|---|---|
RequestContext() context.Context | 返回标准库 Context(自动注入 TraceID/UID/Language) |
SetRequestContext(ctx) | 更新 Request 的 Context |
上下文辅助函数
| 函数 | 说明 |
|---|---|
GetContextTraceID(ctx) string | 获取 TraceID |
SetContextTraceID(ctx, id) | 设置 TraceID |
GetContextUid(ctx) int64 | 获取用户 UID |
SetContextUid(ctx, uid) | 设置用户 UID |
GetContextLanguage(ctx) string | 获取语言 |
SetContextLanguage(ctx, lang) | 设置语言 |
GetTraceIDFromContext(ctx) string | 从 context.Context 获取 TraceID |
GetUidFromContext(ctx) int64 | 从 context.Context 获取 UID |
GetLanguageFromContext(ctx) string | 从 context.Context 获取 Language |