# Go API 开发规范 ## 1. 概述 ### 1.1 目的 本文档旨在为 `bindbox-game` 项目制定一套统一、清晰的 Go API 开发规范。通过标准化代码结构、命名约定、错误处理和文档注释,我们致力于提高代码的可读性、可维护性和团队协作效率,并从根本上避免之前开发中遇到的问题(如包导入不一致、特殊字符使用不当等)。 ### 1.2 适用范围 本规范主要适用于项目 `internal/api/` 目录下的所有 API 层的开发工作。所有新的 API 模块以及对现有模块的修改,都必须严格遵守本规范。 ### 1.3 核心原则 * **一致性**:所有代码都应遵循相同的模式和风格。 * **清晰性**:代码应易于阅读和理解,避免复杂的或晦涩的实现。 * **可维护性**:代码结构应清晰,便于未来扩展和重构。 * **api/service**:代码结构应清晰,api 和 service 应该保持一致的命名规范。必须: 一个 api 对应一个 service。一个函数功能对应一个 service 方法。单独一个文件 * **接口文档注释**:所有 API 端点都必须包含详细的 Swagger 注释,描述请求参数、响应体、可能的错误码等。 * **测试功能**:所有 API 端点都必须包含详细的测试用例,确保功能的正确性和稳定性。(cmd/testchain/ 中写) --- ## 2. 目录与文件结构 规范的目录结构是项目可维护性的基础。 * **模块化目录**:每一个独立的业务功能模块都应在 `internal/api/` 下创建一个专属目录。 ``` internal/api/ ├── admin/ ├── activity/ └── ... (其他模块) ``` * **Handler 结构体定义**:在每个模块目录下,应有一个与模块同名的 `go` 文件(如 `admin.go`),用于定义该模块的 `handler` 结构体和 `New` 初始化函数(注:Handler 不直接进行数据库读写)。 ```go // internal/api/admin/admin.go package admin type handler struct { logger logger.CustomLogger writeDB *dao.Query // 仅用于注入(避免在 Handler 直接使用) readDB *dao.Query // 仅用于注入(避免在 Handler 直接使用) svc adminsvc.Service } func New(logger logger.CustomLogger, db mysql.Repo) *handler { return &handler{ logger: logger, writeDB: dao.Use(db.GetDbW()), readDB: dao.Use(db.GetDbR()), svc: adminsvc.New(logger, db), } } ``` * **端点文件分离**:每个 API 端点(Endpoint)的实现应放在一个独立的 `go` 文件中,文件名应清晰描述其功能,示例对齐当前 `admin` 模块: ``` internal/api/admin/ ├── admin.go // Handler 定义 ├── login.go // 管理员登录 ├── admin_create.go // 新增客服 ├── admin_modify.go // 编辑客服(含路径参数) ├── admin_delete.go // 删除客服(批量) └── admin_list.go // 客服列表(分页查询) ``` --- ## 3. 命名规范 统一的命名规范能极大提升代码的可读性。 * **Go 文件**:使用小写蛇形命名法 (`snake_case`),如 `admin_create.go`。 * **Handler 函数**:使用大驼峰命名法 (`PascalCase`),且函数名应清晰表达其动作,如 `CreateAdmin()`。 * **请求/响应结构体**:使用小驼峰命名法 (`camelCase`),并以 `Request`/`Response` 作为后缀,如 `createAdminRequest`, `createAdminResponse`。 --- ## 4. API Handler 规范 API Handler 是业务逻辑的入口,其规范性至关重要。 ### 4.1 函数签名 所有 API Handler 函数都必须返回 `core.HandlerFunc` 类型。这是框架的核心设计,用于统一处理请求上下文。 ```go // CreateAdmin 新增客服 func (h *handler) CreateAdmin() core.HandlerFunc { return func(ctx core.Context) { // ... 实现 } } ``` ### 4.2 参数绑定与校验 * **绑定(JSON)**:POST/PUT 等带请求体的接口使用 `ctx.ShouldBindJSON(req)` 绑定到预定义的 `Request` 结构体。 * **绑定(Query)**:GET 查询接口使用 `ctx.ShouldBindForm(req)` 绑定到预定义的 `Request` 结构体(字段使用 `form:"..."` tag)。 * **路径参数**:通过 `ctx.Param("id")` 等读取路径参数并进行类型转换与校验。 * **校验**:绑定方法会自动执行 `binding` tag 定义的校验规则。如果校验失败,必须立即中断请求并返回错误。 ```go req := new(createAdminRequest) res := new(createAdminResponse) if err := ctx.ShouldBindJSON(req); err != nil { ctx.AbortWithError(core.Error( http.StatusBadRequest, code.ParamBindError, validation.Error(err)), ) return } ``` ```go // GET 查询绑定示例(与 admin_list 对齐) req := new(listRequest) res := new(listResponse) if err := ctx.ShouldBindForm(req); err != nil { ctx.AbortWithError(core.Error( http.StatusBadRequest, code.ParamBindError, validation.Error(err)), ) return } ``` ```go // 路径参数读取示例(与 admin_modify 对齐) id, err := strconv.Atoi(ctx.Param("id")) if err != nil { ctx.AbortWithError(core.Error( http.StatusBadRequest, code.ParamBindError, "未传递编号ID"), ) return } ``` ### 4.3 响应处理 * **成功响应**:当业务逻辑成功执行后,必须使用 `ctx.Payload(res)` 方法来返回标准的成功响应。`res` 是预定义的 `Response` 结构体实例。 ```go res.Message = "操作成功" ctx.Payload(res) ``` * **错误响应**:见第 6 章“错误处理规范”。 --- ## 5. 数据传输对象 (DTO) 规范 DTO (Data Transfer Object) 是 API 契约的直接体现。 * **结构体定义**:每个 API 都应定义清晰的 `Request` 和 `Response` 结构体。 * **字段注释**:所有结构体字段都必须有清晰的中文注释,解释其含义和用途。 * **JSON Tag**:所有字段都必须包含 `json:"..."` tag,以确保与前端交互的 JSON 字段名一致。 * **Validation Tag**:对于 `Request` 结构体中需要校验的字段,必须添加 `binding:"..."` tag 来定义校验规则(如 `required`)。 ```go // internal/api/admin/admin_create.go type createAdminRequest struct { UserName string `json:"username" binding:"required"` // 用户名 NickName string `json:"nickname" binding:"required"` // 昵称 Password string `json:"password" binding:"required"` // 密码 Mobile string `json:"mobile"` // 手机号 (非必填) } type createAdminResponse struct { Message string `json:"message"` // 提示信息 } ``` ```go // internal/api/admin/admin_list.go // GET 查询示例(字段使用 form 标签) type listRequest struct { Username string `form:"username"` // 用户名 Nickname string `form:"nickname"` // 昵称 Page int `form:"page"` // 当前页码,默认 1 PageSize int `form:"page_size"` // 每页返回的数据量,最多 100 条 } type listData struct { ID int32 `json:"id"` UserName string `json:"username"` NickName string `json:"nickname"` Mobile string `json:"mobile"` Avatar string `json:"avatar"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } type listResponse struct { Page int `json:"page"` PageSize int `json:"page_size"` Total int64 `json:"total"` List []listData `json:"list"` } ``` --- ## 6. 错误处理规范 统一的错误处理是保证 API 健壮性和可预测性的关键。 * **统一入口**:所有在 Handler 中捕获的错误(参数校验失败、数据库操作失败、权限不足等)都必须通过 `ctx.AbortWithError()` 函数来处理。 * **`core.Error` 结构**:`ctx.AbortWithError` 接收一个 `core.Error` 对象,该对象封装了错误的三个核心要素: 1. **HTTP 状态码**:如 `http.StatusBadRequest`。 2. **业务错误码**:在 `internal/code/` 中预定义的 `code.Code`,如 `code.CreateAdminError`。 3. **详细错误信息**:一个描述错误具体原因的字符串。 * **未找到记录**:当数据库查询返回 `gorm.ErrRecordNotFound` 时,需要按具体业务错误码返回明确的提示信息(如“账号不存在”)。 * **权限校验失败**:当 `ctx.SessionUserInfo().IsSuper != 1` 时,禁止操作,返回对应业务错误码和提示“禁止操作”。 ```go // 示例:用户已存在 if info != nil { ctx.AbortWithError(core.Error( http.StatusBadRequest, code.CreateAdminError, fmt.Sprintf("%s: %s", code.Text(code.CreateAdminError), "该账号已存在")), ) return } ``` ```go // 示例:未找到记录(与 admin_modify/login 对齐) if err == gorm.ErrRecordNotFound { ctx.AbortWithError(core.Error( http.StatusBadRequest, code.AdminLoginError, fmt.Sprintf("%s: %s", code.Text(code.AdminLoginError), "账号不存在,请联系管理员。")), ) return } ``` ```go // 示例:权限校验失败(与 create/modify/delete/list 对齐) if ctx.SessionUserInfo().IsSuper != 1 { ctx.AbortWithError(core.Error( http.StatusBadRequest, code.CreateAdminError, fmt.Sprintf("%s: %s", code.Text(code.CreateAdminError), "禁止操作")), ) return } ``` --- ### 6.1 数据库上下文与读写分离(Service 层) * **上下文传递**:所有数据库操作必须调用 `WithContext(ctx)`(Service 层接收 `context.Context`)。 * **读写分离**:读取使用 `s.readDB`,写入使用 `s.writeDB`。 * **分页查询会话**:分页列表与计数需要分别创建独立会话,避免互相影响。 ```go query := s.readDB.Admin.WithContext(ctx) listQueryDB := query.Session(&gorm.Session{}) countQueryDB := query.Session(&gorm.Session{}) resultData, err := listQueryDB.Order(s.readDB.Admin.ID.Desc()).Limit(in.PageSize).Offset((in.Page-1)*in.PageSize).Find() if err != nil { /* 错误处理 */ } count, err := countQueryDB.Count() if err != nil { /* 错误处理 */ } ``` --- ## 7. 日志记录规范 虽然在 `admin` 模块的示例中未显式大量使用,但规范的日志记录对于问题排查至关重要。 * **使用注入的 Logger**:应通过 `handler` 结构体中注入的 `h.logger` 实例来记录日志。 * **记录关键信息**:在关键业务节点或错误发生时,记录必要的上下文信息,如请求参数、用户 ID 等。 ```go // 推荐实践 (示例) h.logger.Error("创建管理员失败", zap.Error(err), zap.String("username", req.UserName)) ``` --- ## 8. 注释与文档 (Swagger) API 的可发现性和易用性依赖于完善的文档。 * **强制 Swagger 注解**:每个 API Handler 函数上方都必须添加完整的 Swagger 注解块。 * **注解内容**:必须包含以下核心注解: * `@Summary`:一句话功能简介。 * `@Description`:更详细的功能描述。 * `@Tags`:API 分组标签,便于在 Swagger UI 中分类。 * `@Accept` / `@Produce`:通常为 `json`。 * `@Param`:描述请求参数,包括参数位置(`body`)、类型、是否必需和说明。 * `@Success`:描述成功响应的结构。 * `@Failure`:描述可能发生的错误响应。 * `@Router`:API 的路由路径和 HTTP 方法。 * `@Security`:如果接口需要认证,需添加此注解。 ```go // internal/api/admin/admin_create.go // CreateAdmin 新增客服 // @Summary 新增客服 // @Description 新增客服 // @Tags 管理端.客服管理 // @Accept json // @Produce json // @Param RequestBody body createAdminRequest true "请求参数" // @Success 200 {object} createAdminResponse // @Failure 400 {object} code.Failure // @Router /api/admin/create [post] // @Security LoginVerifyToken func (h *handler) CreateAdmin() core.HandlerFunc { /* ... */ } // internal/api/admin/admin_list.go(GET 查询示例) // @Param username query string false "用户名" // @Param nickname query string false "昵称" // @Param page query int true "当前页码" default(1) // @Param page_size query int true "每页返回的数据量,最多 100 条" default(20) // @Router /api/admin/list [get] // @Security LoginVerifyToken // internal/api/admin/admin_modify.go(路径参数示例) // @Param id path string true "编号ID" // @Router /api/admin/{id} [put] // @Security LoginVerifyToken // internal/api/admin/login.go(登录接口不加 Security) // @Router /api/admin/login [post] ``` --- ## 9. 路由与中间件规范 路由是 API 对外的入口,必须与模块职责、认证策略保持一致,并在 `internal/router/router.go` 统一注册。 ### 9.1 分组与路径前缀 - **管理端**:统一前缀 `"/api/admin"` - 非认证组:用于登录、系统状态等不需要鉴权的接口。 - 认证组:使用鉴权中间件保护,适用于敏感操作(新增、编辑、删除、列表)。 - **用户端(APP)**:统一前缀 `"/api/app"`,用于对外业务接口(如活动相关)。 ### 9.2 鉴权中间件 - **封装方式**:通过 `core.WrapAuthHandler(interceptor.AdminTokenAuthVerify)` 将会话信息写入上下文,认证失败统一返回业务错误。 - **接口签名**:`internal/router/interceptor/interceptor.go:12-18` 定义了鉴权接口: `AdminTokenAuthVerify(ctx core.Context) (sessionUserInfo proposal.SessionUserInfo, err core.BusinessError)`。 - **实现参考**:管理端鉴权实现见 `internal/router/interceptor/admin_auth.go:1-80`。 ### 9.3 路由注册示例(与当前 admin 模块对齐) ```go // internal/router/router.go // 管理端非认证接口路由组(登录) adminNonAuthApiRouter := mux.Group("/api/admin") { adminNonAuthApiRouter.GET("/license/status", func(ctx core.Context) { /* ... */ }) adminNonAuthApiRouter.POST("/login", adminHandler.Login()) } // 管理端认证接口路由组(需要 Authorization) adminAuthApiRouter := mux.Group("/api/admin", core.WrapAuthHandler(intc.AdminTokenAuthVerify)) { adminAuthApiRouter.POST("/create", adminHandler.CreateAdmin()) adminAuthApiRouter.PUT("/:id", adminHandler.ModifyAdmin()) adminAuthApiRouter.POST("/delete", adminHandler.DeleteAdmin()) adminAuthApiRouter.GET("/list", adminHandler.PageList()) } // APP 端接口路由组 appApiRouter := mux.Group("/api/app") { appHandler := app.New(logger, db) appApiRouter.GET("/activities", appHandler.ListActivities()) // ... 其他 APP 端接口 } ``` ### 9.4 Swagger 安全定义与使用 - **安全定义位置**:`main.go:23-26` 定义认证方案 `LoginVerifyToken`,从请求头读取 `Authorization`。 - **注解使用规范**: - 管理端认证组接口均需添加 `@Security LoginVerifyToken`。 - 登录接口不需要添加 `@Security` 注解。 ### 9.5 路径命名与语义 - **资源语义**: - `POST /api/admin/create`:创建资源(支持复杂校验与默认值设置)。 - `PUT /api/admin/:id`:更新单个资源,路径参数 `id` 必须有效。 - `POST /api/admin/delete`:批量删除,参数 `ids` 以逗号分隔。 - `GET /api/admin/list`:分页检索,`page`/`page_size` 合理限制(最大 100)。 - **用户端**:保持 RESTful 风格,如 `GET /api/app/activities`、`GET /api/app/activities/:activity_id`。 ### 9.6 指标与别名(可选) - 为避免路径参数导致指标维度爆炸,可在 Handler 前添加别名记录指标: ```go // 例如:为 /api/app/activities/:activity_id 设置指标记录别名 appApiRouter.GET("/activities/:activity_id", core.AliasForRecordMetrics("/api/app/activities/:activity_id"), appHandler.GetActivityDetail(), ) ``` --- ## 10. 服务层分离规范 为避免 Handler 同时承担传输与业务职责,必须在 `internal/service/{module}` 引入服务层,统一承载业务规则、数据协作与事务处理。 ### 10.1 目录与命名 - 目录:`internal/service/admin`、`internal/service/activity` 等按模块划分。 - 包名:与模块名一致,Handler 导入时使用别名避免冲突(如 `svc "bindbox-game/internal/service/admin"`)。 ### 10.1.1 文件拆分(与 API 端点保持一致) - 每个端点在服务层对应一个实现文件,避免所有方法堆叠在单文件: ``` internal/service/admin/ ├── service.go // 接口、构造 ├── login.go // 登录 ├── create.go // 新增客服 ├── modify.go // 编辑客服 ├── delete.go // 删除客服 └── list.go // 客服列表 ``` - 规则:每个端点的输入/输出类型在对应文件中定义(例如 `LoginInput`/`LoginResult` 在 `login.go`)。 --- ## 11. 流程规范(Router → API → Service) ### 11.1 职责边界 - Router:负责分组、认证拦截与端点注册;不承载业务逻辑。 - 管理端非认证路由:`internal/router/router.go:44-51` - 管理端认证路由(鉴权中间件):`internal/router/router.go:53-60` - 用户端 APP 路由:`internal/router/router.go:62-72` - API(Handler):负责参数绑定、权限校验、错误码映射与响应输出;不直接操作数据库。 - 绑定参数并校验:如登录 `internal/api/admin/login.go:35-45`、列表 `internal/api/admin/admin_list.go:57-64` - 权限校验:如列表 `internal/api/admin/admin_list.go:83-90`、创建 `internal/api/admin/admin_create.go:49-56` - 错误码映射与响应:如登录 `internal/api/admin/login.go:53-59,61-64` - Service:承载领域逻辑、数据访问与事务;输入/输出类型在对应端点文件中定义。 - 登录实现:`internal/service/admin/login.go:16-52` - 创建实现:`internal/service/admin/admin_create.go:14-42` - 编辑实现:`internal/service/admin/admin_modify.go:13-50` - 删除实现:`internal/service/admin/admin_delete.go:9-15` - 列表实现:`internal/service/admin/admin_list.go:12-38` ### 11.2 端到端流程(以管理端为例) - Router 接收请求并路由到对应分组:非认证或认证(携带 `Authorization`)。 - 若为认证接口,鉴权中间件注入会话信息:`internal/router/interceptor/admin_auth.go:1-80`。 - Handler: - `ShouldBindJSON` 或 `ShouldBindForm` 解析请求(含路径参数 `ctx.Param`)。 - 权限检查(如 `ctx.SessionUserInfo().IsSuper`)。 - 将 DTO 转换为 Service 输入,调用 `svc.*(ctx.RequestContext(), input)`。 - 根据 Service 返回的错误映射业务错误码,或输出成功响应 `ctx.Payload(res)`。 - Service: - 使用 `dao.Query.WithContext` 与读写分离实现查询或写入。 - 必要场景内使用事务保障原子性(参考 `dao.gen.go` 的 `Query.Transaction`)。 ### 11.3 错误处理策略 - Service 返回语义化错误信息(`error`);不直接决定 HTTP 或业务码。 - Handler 统一映射到 `core.Error(httpCode, code.* , message)` 并 `AbortWithError`。 - 登录错误映射:`internal/api/admin/login.go:53-59` - 创建错误映射:`internal/api/admin/admin_create.go:66-71` - 编辑错误映射:`internal/api/admin/admin_modify.go:87-93` - 删除错误映射:`internal/api/admin/admin_delete.go:95-102` - 列表错误映射:`internal/api/admin/admin_list.go:98-105` ### 11.4 数据绑定与类型转换 - 登录:`loginRequest` → `LoginInput` 映射:`internal/api/admin/login.go:47-51` - 创建:`createAdminRequest` → `CreateInput` 映射:`internal/api/admin/admin_create.go:58-65` - 编辑:`modifyAdminRequest` → `ModifyInput` 映射:`internal/api/admin/admin_modify.go:79-86` - 列表:`listRequest(form)` → `ListInput` 映射:`internal/api/admin/admin_list.go:92-97` - 删除:解析 `ids` 列表后直接传入 `svc.Delete`:`internal/api/admin/admin_delete.go:95` ### 11.5 事务与读写分离 - 读操作统一使用 `readDB`,写操作统一使用 `writeDB`。 - 所有 DAO 调用必须附带上下文:`WithContext(ctx)`。 - 多步骤写操作建议在 Service 层使用事务(`Query.Transaction`):`internal/repository/mysql/dao/gen.go:211-213`。 ### 11.6 鉴权与会话 - 路由层通过 `core.WrapAuthHandler(intc.AdminTokenAuthVerify)` 进行会话注入:`internal/router/router.go:53-60`。 - 鉴权逻辑参考:`internal/router/interceptor/admin_auth.go:1-80`。 - Handler 使用 `ctx.SessionUserInfo()` 获取操作者信息(如 `CreatedBy`、`UpdatedBy`)。 ### 11.7 命名与拆分 - API 与 Service 均按端点拆分文件,命名对齐,便于定位与维护。 - Service 的输入/输出类型在对应端点文件定义(如 `login.go` 定义 `LoginInput/LoginResult`)。 --- ## 12. Repository 规范 ### 12.1 层职责 - 负责数据访问(CRUD、分页、事务),不包含业务规则与 HTTP 细节。 - 仅依赖底层 DAO/Model,向 Service 层提供稳定的数据操作能力。 ### 12.2 目录与组件 - 目录:`internal/repository/mysql` - `dao/`:由 `gorm/gen` 生成的查询对象与读写分离封装,参考: - 读写分离方法:`internal/repository/mysql/dao/gen.go:135-143` - 事务封装:`internal/repository/mysql/dao/gen.go:211-213` - 上下文绑定:`internal/repository/mysql/dao/gen.go:188-209` - `model/`:数据库对应的实体结构体(字段、标签)。 ### 12.3 使用规范(在 Service 层) - 初始化查询对象: - 读库:`readDB := dao.Use(db.GetDbR())` - 写库:`writeDB := dao.Use(db.GetDbW())` - 所有操作必须绑定上下文:`WithContext(ctx)`,示例(查询管理员): ```go info, err := s.readDB.Admin.WithContext(ctx). Where(s.readDB.Admin.Username.Eq(in.Username)). First() ``` - 分页与计数使用独立会话,避免互相影响: ```go listQueryDB := query.Session(&gorm.Session{}) countQueryDB := query.Session(&gorm.Session{}) items, _ := listQueryDB.Order(...).Limit(...).Offset(...).Find() total, _ := countQueryDB.Count() ``` - 多步骤写操作建议在事务中完成: ```go _ = s.writeDB.Transaction(func(tx *dao.Query) error { // 在 tx 上执行写操作 return nil }) ``` ### 12.4 分层建议 - 如需进一步解耦 DAO,可在 `internal/repository/{module}` 定义模块级仓库接口(如 `adminrepo`),内部封装 DAO 细节;Service 只依赖仓库接口,便于单元测试与替换数据源。 - 目前项目直接在 Service 层使用 DAO,符合轻量项目场景;当跨表协作复杂时可引入仓库接口以降低耦合。