mcpField
G
go-chi-handler

go-chi-handler

github.com/cristiano-pacheco开发147 次安装1 天前更新
代码仓库

关于 go-chi-handler

go-chi-handler 是面向 Go 后端开发的编码技能,用于生成 Chi HTTP 处理器。它规定了 DTO 映射、错误处理、Swagger 注解和 Fx 接线等项目约定。

主要功能

  1. 01生成 REST HTTP 处理器
  2. 02映射请求与响应 DTO
  3. 03添加 Swagger 注解
  4. 04遵循统一错误处理模式
  5. 05提供 Fx 接线步骤

适用场景

  1. 01为 REST 操作创建处理器
  2. 02实现列表、创建、更新和删除端点
  3. 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 注解规则

  1. @Summary:简要说明操作(例如,“列出资源”“创建资源”)
  2. @Description:完整描述端点的功能
  3. @Tags:复数形式的资源名称(例如,“资源”“联系人”)
  4. @Accept:始终为 json
  5. @Produce:始终为 json
  6. @Security:需要身份验证时添加 BearerAuth
  7. @Param:定义路径参数和请求体
    • 路径参数:@Param id path int true "Resource ID"
    • 请求体:@Param request body dto.CreateResourceRequest true "Resource data"
  8. @Success:状态码和响应类型
    • 200:{object} response.Envelope[dto.ResourceResponse]
    • 201:{object} response.Envelope[dto.CreateResourceResponse]
    • 204:无内容,仅使用描述字符串
  9. @Failure:常见错误(404、422、500),使用 {object} brickserrs.Error(注意别名)
  10. @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),

处理器通常会提供给路由器,而不是作为端口暴露。

关键规则

  1. 不允许独立函数:当文件中包含带方法的结构体时,绝不添加包级独立函数。所有辅助逻辑都必须是结构体上的私有方法(例如 parseUintPathParam、newBadRequestError)。
  2. 结构体:包含所有用例、response.ErrorHandler 和 logger.Logger
  3. 构造函数:必须返回指针 *ResourceHandler
  4. 上下文:始终从请求中获取:ctx := r.Context()
  5. 请求解码:使用 request.ReadJSON(w, r, &dto)——先声明变量,再调用 ReadJSON
  6. URL 参数:通过私有的 parseUintPathParam 方法解析,使用 chi.URLParam + strconv.ParseUint
  7. 错误处理:始终调用 h.logger.Error(...) 和 h.errorHandler.Error(w, err),然后返回——适用于所有错误,包括验证/输入解析错误
  8. 响应映射:将用例输出映射到 DTO;绝不直接返回用例输出
  9. 成功响应:
    • 列表/获取:response.JSON(w, http.StatusOK, data, http.Header{})
    • 创建:response.JSON(w, http.StatusCreated, data, http.Header{})
    • 更新/删除:response.NoContent(w)
  10. Swagger:必须为每个处理器方法添加完整的 swagger 注解;在 @Failure 行中使用 brickserrs.Error(而不是 errs.Error)
  11. 不添加注释:不要在方法体内添加多余注释
  12. 验证:生成后运行 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"

工作流

  1. 创建包含用例依赖项的处理器结构体(使用 ucdecorator.UseCase[Input, Output])
  2. 实现构造函数 NewResourceHandler
  3. 按照上述模式实现带 Handle 前缀的处理器方法
  4. 如需路径参数,添加私有的 parseUintPathParam + newBadRequestError 方法
  5. 为所有方法添加 swagger 注解(在 @Failure 中使用 brickserrs.Error)
  6. 在模块的 fx.go 中添加 Fx wiring
  7. 运行 make lint 和 make nilaway,验证静态测试
  8. 运行 make update-swagger,重新生成 swagger 文档

评论

支持 Markdown