关于 go-chi-handler
go-chi-handler 是面向 Go 后端开发的编码技能,用于生成 Chi HTTP 处理器。它规定了 DTO 映射、错误处理、Swagger 注解和 Fx 接线等项目约定。
主要功能
- 01生成 REST HTTP 处理器
- 02映射请求与响应 DTO
- 03添加 Swagger 注解
- 04遵循统一错误处理模式
- 05提供 Fx 接线步骤
适用场景
- 01为 REST 操作创建处理器
- 02实现列表、创建、更新和删除端点
- 03为按 ID 查询的端点解析路径参数
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ cristiano-pacheco/ ai-tools --skill go-chi-handler安装后在 Claude Code 中输入/go-chi-handler确认已加载。
详细介绍
Go Chi 处理器
为 Go 后端生成 Chi HTTP 处理器实现。
适用场景
- 为 REST 操作创建 HTTP 端点处理器
- List、Create、Update、Delete、Get 处理器
- 通过用例编排进行请求/响应 DTO 映射
- 带 Swagger 注解的端点
处理器结构
位置:internal/modules/<module>/http/chi/handler/<resource>_handler.go
package handler
import (
"net/http"
"fmt"
"strconv"
"strings"
brickserrs "github.com/cristiano-pacheco/bricks/pkg/errs"
"github.com/cristiano-pacheco/bricks/pkg/http/request"
"github.com/cristiano-pacheco/bricks/pkg/http/response"
"github.com/cristiano-pacheco/bricks/pkg/logger"
"github.com/cristiano-pacheco/bricks/pkg/ucdecorator"
"github.com/cristiano-pacheco/pingo/internal/modules/<module>/http/dto"
"github.com/cristiano-pacheco/pingo/internal/modules/<module>/usecase"
"github.com/go-chi/chi/v5"
)
type ResourceHandler struct {
resourceCreateUseCase ucdecorator.UseCase[usecase.ResourceCreateInput, usecase.ResourceCreateOutput]
resourceListUseCase ucdecorator.UseCase[usecase.ResourceListInput, usecase.ResourceListOutput]
resourceUpdateUseCase ucdecorator.UseCase[usecase.ResourceUpdateInput, usecase.ResourceUpdateOutput]
resourceDeleteUseCase ucdecorator.UseCase[usecase.ResourceDeleteInput, usecase.ResourceDeleteOutput]
resourceGetUseCase ucdecorator.UseCase[usecase.ResourceGetInput, usecase.ResourceGetOutput]
errorHandler response.ErrorHandler
logger logger.Logger
}
func NewResourceHandler(
resourceCreateUseCase ucdecorator.UseCase[usecase.ResourceCreateInput, usecase.ResourceCreateOutput],
resourceListUseCase ucdecorator.UseCase[usecase.ResourceListInput, usecase.ResourceListOutput],
resourceUpdateUseCase ucdecorator.UseCase[usecase.ResourceUpdateInput, usecase.ResourceUpdateOutput],
resourceDeleteUseCase ucdecorator.UseCase[usecase.ResourceDeleteInput, usecase.ResourceDeleteOutput],
resourceGetUseCase ucdecorator.UseCase[usecase.ResourceGetInput, usecase.ResourceGetOutput],
errorHandler response.ErrorHandler,
logger logger.Logger,
) *ResourceHandler {
return &ResourceHandler{
resourceCreateUseCase: resourceCreateUseCase,
resourceListUseCase: resourceListUseCase,
resourceUpdateUseCase: resourceUpdateUseCase,
resourceDeleteUseCase: resourceDeleteUseCase,
resourceGetUseCase: resourceGetUseCase,
errorHandler: errorHandler,
logger: logger,
}
}
要点:
- 用例始终使用
ucdecorator.UseCase[Input, Output]泛型接口,绝不使用具体的*usecase.ResourceUseCase指针 - 导入
brickserrs并使用别名:brickserrs "github.com/cristiano-pacheco/bricks/pkg/errs" - 构造函数返回指针
*ResourceHandler
DTO(数据传输对象)
请求和响应 DTO 定义在 internal/modules/<module>/http/dto/<resource>_dto.go 中。
典型 DTO 结构:
package dto
type CreateResourceRequest struct {
Field1 string `json:"field1"`
Field2 int `json:"field2"`
}
type CreateResourceResponse struct {
ID uint64 `json:"id"`
Field1 string `json:"field1"`
Field2 int `json:"field2"`
}
type UpdateResourceRequest struct {
Field1 string `json:"field1"`
Field2 int `json:"field2"`
}
type ResourceResponse struct {
ID uint64 `json:"id"`
Field1 string `json:"field1"`
Field2 int `json:"field2"`
}
要点:
- DTO 位于 HTTP 传输层,与用例输入或模型分开
- 使用 JSON 标签进行序列化
- DTO 应专注于 HTTP 契约,而非领域逻辑
处理器方法模式
所有处理器方法都使用 Handle 前缀:HandleListResources、HandleCreateResource 等。
列表(GET /resources)
// @Summary List resources
// @Description Retrieves all resources
// @Tags Resources
// @Accept json
// @Produce json
// @Success 200 {object} response.Envelope[[]dto.ResourceResponse] "Successfully retrieved resources"
// @Failure 500 {object} brickserrs.Error "Internal server error"
// @Router /api/v1/resources [get]
func (h *ResourceHandler) HandleListResources(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
output, err := h.resourceListUseCase.Execute(ctx, usecase.ResourceListInput{})
if err != nil {
h.logger.Error("failed to list resources", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
resources := make([]dto.ResourceResponse, 0, len(output.Resources))
for _, resource := range output.Resources {
resources = append(resources, dto.ResourceResponse{
ID: resource.ID,
Name: resource.Name,
// ... map other fields
})
}
if err = response.JSON(w, http.StatusOK, resources, http.Header{}); err != nil {
h.logger.Error("failed to write list resources response", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
}
创建(POST /resources)
// @Summary Create resource
// @Description Creates a new resource
// @Tags Resources
// @Accept json
// @Produce json
// @Param request body dto.CreateResourceRequest true "Resource data"
// @Success 201 {object} response.Envelope[dto.CreateResourceResponse] "Successfully created resource"
// @Failure 422 {object} brickserrs.Error "Invalid request format or validation error"
// @Failure 500 {object} brickserrs.Error "Internal server error"
// @Router /api/v1/resources [post]
func (h *ResourceHandler) HandleCreateResource(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var createRequest dto.CreateResourceRequest
if err := request.ReadJSON(w, r, &createRequest); err != nil {
h.logger.Error("failed to parse request body", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
output, err := h.resourceCreateUseCase.Execute(ctx, usecase.ResourceCreateInput{
Name: createRequest.Name,
// ... map other fields
})
if err != nil {
h.logger.Error("failed to create resource", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
createResponse := dto.CreateResourceResponse{
ID: output.ID,
Name: output.Name,
// ... map other fields
}
if err = response.JSON(w, http.StatusCreated, createResponse, http.Header{}); err != nil {
h.logger.Error("failed to write create resource response", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
}
更新(PUT /resources/:id)
// @Summary Update resource
// @Description Updates an existing resource
// @Tags Resources
// @Accept json
// @Produce json
// @Param id path int true "Resource ID"
// @Param request body dto.UpdateResourceRequest true "Resource data"
// @Success 204 "Successfully updated resource"
// @Failure 422 {object} brickserrs.Error "Invalid request format or validation error"
// @Failure 404 {object} brickserrs.Error "Resource not found"
// @Failure 500 {object} brickserrs.Error "Internal server error"
// @Router /api/v1/resources/{id} [put]
func (h *ResourceHandler) HandleUpdateResource(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id, err := h.parseUintPathParam(r, "id")
if err != nil {
h.logger.Error("invalid resource id", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
var updateRequest dto.UpdateResourceRequest
if err = request.ReadJSON(w, r, &updateRequest); err != nil {
h.logger.Error("failed to parse request body", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
if _, err = h.resourceUpdateUseCase.Execute(ctx, usecase.ResourceUpdateInput{
ID: id,
Name: updateRequest.Name,
// ... map other fields
}); err != nil {
h.logger.Error("failed to update resource", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
response.NoContent(w)
}
删除(DELETE /resources/:id)
// @Summary Delete resource
// @Description Deletes an existing resource
// @Tags Resources
// @Accept json
// @Produce json
// @Param id path int true "Resource ID"
// @Success 204 "Successfully deleted resource"
// @Failure 404 {object} brickserrs.Error "Resource not found"
// @Failure 500 {object} brickserrs.Error "Internal server error"
// @Router /api/v1/resources/{id} [delete]
func (h *ResourceHandler) HandleDeleteResource(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id, err := h.parseUintPathParam(r, "id")
if err != nil {
h.logger.Error("invalid resource id", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
if _, err = h.resourceDeleteUseCase.Execute(ctx, usecase.ResourceDeleteInput{ID: id}); err != nil {
h.logger.Error("failed to delete resource", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
response.NoContent(w)
}
按 ID 获取(GET /resources/:id)
// @Summary Get resource
// @Description Retrieves a resource by ID
// @Tags Resources
// @Accept json
// @Produce json
// @Param id path int true "Resource ID"
// @Success 200 {object} response.Envelope[dto.ResourceResponse] "Successfully retrieved resource"
// @Failure 404 {object} brickserrs.Error "Resource not found"
// @Failure 500 {object} brickserrs.Error "Internal server error"
// @Router /api/v1/resources/{id} [get]
func (h *ResourceHandler) HandleGetResource(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id, err := h.parseUintPathParam(r, "id")
if err != nil {
h.logger.Error("invalid resource id", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
output, err := h.resourceGetUseCase.Execute(ctx, usecase.ResourceGetInput{ID: id})
if err != nil {
h.logger.Error("failed to get resource", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
resourceResponse := dto.ResourceResponse{
ID: output.ID,
Name: output.Name,
// ... map other fields
}
if err = response.JSON(w, http.StatusOK, resourceResponse, http.Header{}); err != nil {
h.logger.Error("failed to write get resource response", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
}
URL 参数辅助方法(私有方法)
解析路径参数的处理器应使用结构体上的私有方法,而非独立函数:
func (h *ResourceHandler) parseUintPathParam(r *http.Request, paramName string) (uint64, error) {
value := strings.TrimSpace(chi.URLParam(r, paramName))
if value == "" {
return 0, h.newBadRequestError(fmt.Sprintf("missing path param %q", paramName))
}
parsed, err := strconv.ParseUint(value, 10, 64)
if err != nil {
return 0, h.newBadRequestError(fmt.Sprintf("invalid path param %q", paramName))
}
return parsed, nil
}
func (h *ResourceHandler) newBadRequestError(message string) *brickserrs.Error {
return brickserrs.New("MODULE_90", message, http.StatusBadRequest, nil)
}
请求/响应映射
处理器方法用于衔接 HTTP 请求/响应(来自 internal/modules/<module>/http/dto 的 DTO)与用例输入/输出。
请求映射为用例输入
// Decode request
var req dto.CreateResourceRequest
err := request.ReadJSON(w, r, &req)
if err != nil {
h.logger.Error("failed to parse request body", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
// Map to use case input
input := usecase.ResourceCreateInput{
Field1: req.Field1,
Field2: req.Field2,
}
用例输出映射为响应
// Execute use case
output, err := h.resourceCreateUseCase.Execute(ctx, input)
if err != nil {
h.logger.Error("failed to create resource", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
// Map to response DTO
response := dto.CreateResourceResponse{
ID: output.ID,
Field1: output.Field1,
Field2: output.Field2,
}
Swagger 注解规则
- @Summary:简要说明操作(例如,“列出资源”“创建资源”)
- @Description:完整描述端点的功能
- @Tags:复数形式的资源名称(例如,“资源”“联系人”)
- @Accept:始终为
json - @Produce:始终为
json - @Security:需要身份验证时添加
BearerAuth - @Param:定义路径参数和请求体
- 路径参数:
@Param id path int true "Resource ID" - 请求体:
@Param request body dto.CreateResourceRequest true "Resource data"
- 路径参数:
- @Success:状态码和响应类型
- 200:
{object} response.Envelope[dto.ResourceResponse] - 201:
{object} response.Envelope[dto.CreateResourceResponse] - 204:无内容,仅使用描述字符串
- 200:
- @Failure:常见错误(404、422、500),使用
{object} brickserrs.Error(注意别名) - @Router:
/api/v1/resources/{id} [method]
错误处理模式
每个处理器方法中的所有错误都必须遵循此确切模式,不得例外,包括输入验证和 URL 参数解析错误:
if err != nil {
h.logger.Error("descriptive error message", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
标准错误消息:
"failed to parse request body"— JSON 解码错误"invalid resource id"— URL 参数解析错误"failed to list resources"— List 用例错误"failed to create resource"— Create 用例错误"failed to update resource"— Update 用例错误"failed to delete resource"— Delete 用例错误"failed to get resource"— Get 用例错误"failed to write list resources response"— JSON 响应写入错误(包含操作名称)
Fx 接线
添加到 internal/modules/<module>/fx.go:
fx.Provide(handler.NewResourceHandler),
处理器通常会提供给路由器,而不是作为端口暴露。
关键规则
- 不允许独立函数:当文件中包含带方法的结构体时,绝不添加包级独立函数。所有辅助逻辑都必须是结构体上的私有方法(例如
parseUintPathParam、newBadRequestError)。 - 结构体:包含所有用例、
response.ErrorHandler和logger.Logger - 构造函数:必须返回指针
*ResourceHandler - 上下文:始终从请求中获取:
ctx := r.Context() - 请求解码:使用
request.ReadJSON(w, r, &dto)——先声明变量,再调用 ReadJSON - URL 参数:通过私有的
parseUintPathParam方法解析,使用chi.URLParam+strconv.ParseUint - 错误处理:始终调用
h.logger.Error(...)和h.errorHandler.Error(w, err),然后返回——适用于所有错误,包括验证/输入解析错误 - 响应映射:将用例输出映射到 DTO;绝不直接返回用例输出
- 成功响应:
- 列表/获取:
response.JSON(w, http.StatusOK, data, http.Header{}) - 创建:
response.JSON(w, http.StatusCreated, data, http.Header{}) - 更新/删除:
response.NoContent(w)
- 列表/获取:
- Swagger:必须为每个处理器方法添加完整的 swagger 注解;在
@Failure行中使用brickserrs.Error(而不是errs.Error) - 不添加注释:不要在方法体内添加多余注释
- 验证:生成后运行
make lint和make update-swagger
反模式(绝不可采用)
以下模式曾出现在代码库中,必须避免重复:
❌ 使用原始的 w.WriteHeader,而不是 response.NoContent
// BAD
w.WriteHeader(http.StatusNoContent)
// GOOD
response.NoContent(w)
❌ 在处理器文件中使用包级独立函数
// BAD — standalone functions at package level violate the no-standalone-functions rule
func isResourceEmpty(output usecase.ResourceGetOutput) bool {
return output.Name == ""
}
func firstNonEmpty(values ...string) string { ... }
// GOOD — private methods on the handler struct
func (h *ResourceHandler) isResourceEmpty(output usecase.ResourceGetOutput) bool {
return output.Name == ""
}
func (h *ResourceHandler) firstNonEmpty(values ...string) string { ... }
❌ 验证/解析错误时,在调用 errorHandler 前未调用 logger
// BAD — skips the logger
if parseErr != nil {
h.errorHandler.Error(w, brickserrs.New("MODULE_90", "invalid id", http.StatusBadRequest, nil))
return
}
// GOOD — always log before handling
if parseErr != nil {
h.logger.Error("invalid resource id", logger.Error(parseErr))
h.errorHandler.Error(w, parseErr)
return
}
❌ 在处理器方法体内内联调用 brickserrs.New() 并硬编码错误代码
错误必须定义在模块的 errs/ 包中(使用 go-error skill),不得在处理器方法体内临时创建。
// BAD — hardcoded error codes inline in handler logic
h.errorHandler.Error(w, brickserrs.New("CATALOG_90", "invalid category_id", http.StatusBadRequest, nil))
// GOOD — reference a named error from the module's errs/ package
h.errorHandler.Error(w, errs.ErrInvalidCategoryID)
例外是用于 URL 参数解析的私有方法 newBadRequestError——这是唯一允许内联构造错误的位置,且必须限定在结构体的辅助方法中。
❌ 在结构体字段和构造函数中使用具体用例类型
// BAD — concrete pointer types
type ResourceHandler struct {
resourceCreateUseCase *usecase.ResourceCreateUseCase
}
// GOOD — generic ucdecorator interface
type ResourceHandler struct {
resourceCreateUseCase ucdecorator.UseCase[usecase.ResourceCreateInput, usecase.ResourceCreateOutput]
}
❌ 处理器方法名称没有 Handle 前缀
// BAD
func (h *ResourceHandler) ListResources(w http.ResponseWriter, r *http.Request) { ... }
func (h *ResourceHandler) CreateResource(w http.ResponseWriter, r *http.Request) { ... }
// GOOD
func (h *ResourceHandler) HandleListResources(w http.ResponseWriter, r *http.Request) { ... }
func (h *ResourceHandler) HandleCreateResource(w http.ResponseWriter, r *http.Request) { ... }
❌ 在 swagger @Failure 注解中使用错误的错误类型
// BAD — errs.Error is not the correct reference
// @Failure 500 {object} errs.Error "Internal server error"
// GOOD — use the brickserrs alias
// @Failure 500 {object} brickserrs.Error "Internal server error"
工作流
- 创建包含用例依赖项的处理器结构体(使用
ucdecorator.UseCase[Input, Output]) - 实现构造函数
NewResourceHandler - 按照上述模式实现带
Handle前缀的处理器方法 - 如需路径参数,添加私有的
parseUintPathParam+newBadRequestError方法 - 为所有方法添加 swagger 注解(在
@Failure中使用brickserrs.Error) - 在模块的
fx.go中添加 Fx wiring - 运行
make lint和make nilaway,验证静态测试 - 运行
make update-swagger,重新生成 swagger 文档