Skip to content

控制器

介绍

控制器是组件结构,实现一个或多个处理器。它们是表示层(HTTP/REST)的一部分。它们的职责是:

  • 从请求中获取数据并正确格式化:原始请求数据被转换为 DTO
  • 将其发送给处理业务逻辑并返回 DTO 的服务
  • 使用服务返回的结果构建 HTTP 响应。

因此,控制器本身不应实现业务逻辑。相反,它们依赖于服务

每个功能或资源应有自己的包。例如,如果您有一个处理用户注册、用户档案等的控制器,您应该创建一个 http/controller/user 包。

INFO

因为控制器是组件,它们通过组合 goyave.Component 可以访问服务器的所有基本资源。

以下是一个简单 CRUD 控制器的完整示例。示例中不包含服务实现、DTO 结构和验证规则集。

go
// http/controller/product/product.go
package product

import (
	"context"
	"net/http"
	"strconv"

	"goyave.dev/goyave/v5"
	"goyave.dev/goyave/v5/database"
	"goyave.dev/goyave/v5/util/typeutil"
	"my-project/dto"
	"my-project/service"
)

type Service interface {
	GetByID(ctx context.Context, id int64) (*dto.Product, error)
	Paginate(ctx context.Context, page int, pageSize int) (*database.PaginatorDTO[*dto.User], error)
	Create(ctx context.Context, createDTO *dto.CreateProduct) (*dto.Product, error)
	Update(ctx context.Context, id int64, updateDTO *dto.UpdateProduct) error
	Delete(ctx context.Context, id int64) error
}

type Controller struct {
	goyave.Component
	ProductService Service
}

func (ctrl *Controller) Init(server *goyave.Server) {
	ctrl.ProductService = server.Service(service.Product).(Service)
	ctrl.Component.Init(server)
}

func (ctrl *Controller) RegisterRoutes(router *goyave.Router) {
	subrouter := router.Subrouter("/products")

	subrouter.Get("/", ctrl.Index).ValidateQuery(IndexRequest)
	subrouter.Post("/", ctrl.Create).ValidateBody(CreateRequest)
	subrouter.Get("/{productID:[0-9+]}", ctrl.Show)
	subrouter.Patch("/{productID:[0-9+]}", ctrl.Update).ValidateBody(UpdateRequest)
	subrouter.Delete("/{productID:[0-9+]}", ctrl.Delete)
}

func (ctrl *Controller) Index(response *goyave.Response, request *goyave.Request) {
	query := typeutil.MustConvert[*dto.Index](request.Query)

	paginator, err := ctrl.ProductService.Paginate(request.Context(), query.Page.Default(1), query.PerPage.Default(20))
	if response.WriteDBError(err) {
		return
	}

	response.JSON(http.StatusOK, paginator)
}

func (ctrl *Controller) Show(response *goyave.Response, request *goyave.Request) {
	productID, err := strconv.ParseInt(request.RouteParams["productID"], 10, 64)
	if err != nil {
		response.Status(http.StatusNotFound)
		return
	}

	user, err := ctrl.ProductService.GetByID(request.Context(), productID)
	if response.WriteDBError(err) {
		return
	}

	response.JSON(http.StatusOK, user)
}

func (ctrl *Controller) Create(response *goyave.Response, request *goyave.Request) {
	createDTO := typeutil.MustConvert[*dto.CreateProduct](request.Data)

	product, err := ctrl.ProductService.Create(request.Context(), createDTO)
	if response.WriteDBError(err) {
		return
	}

	response.JSON(http.StatusCreated, map[string]int64{"id": product.ID})
}

func (ctrl *Controller) Update(response *goyave.Response, request *goyave.Request) {
	productID, err := strconv.ParseInt(request.RouteParams["productID"], 10, 64)
	if err != nil {
		response.Status(http.StatusNotFound)
		return
	}

	updateDTO := typeutil.MustConvert[*dto.UpdateProduct](request.Data)

	err = ctrl.ProductService.Update(request.Context(), productID, updateDTO)
	response.WriteDBError(err)
}

func (ctrl *Controller) Delete(response *goyave.Response, request *goyave.Request) {
	productID, err := strconv.ParseInt(request.RouteParams["productID"], 10, 64)
	if err != nil {
		response.Status(http.StatusNotFound)
		return
	}

	err = ctrl.ProductService.Delete(request.Context(), productID)
	response.WriteDBError(err)
}
  • 首先,我们定义 Service 接口,它代表控制器的依赖项
  • Init() 中,我们从服务器的服务容器中获取它:server.Service(service.Product).(Service)Init() 是使用 router.Controller() 时由框架自动调用的函数。还有另一种注入依赖的方法,在下面解释。了解更多关于服务的信息请点击这里
  • UpdateDelete 的末尾不需要添加 response.Status(http.StatusNoContent),因为如果响应体为空且未设置状态,框架会自动将响应状态设置为 204
  • 设置 Content-Type 标头不是必需的。response.Write 会自动检测内容类型并相应设置标头,如果后者尚未定义。

注意

为了使用包含已解析请求体的 request.Data,您需要添加解析中间件

go
import "goyave.dev/goyave/v5/middleware/parse"

router.GlobalMiddleware(&parse.Middleware{})

命名约定

  • 控制器包根据它们主要使用的资源命名,使用单数形式。例如,Product 模型的控制器将称为 http/controller/product。如果控制器与资源无关,则给它一个表达性强的名称。
  • 为了避免重复,控制器结构总是命名为 Controller。这样,从主路由注册器创建控制器是清晰且易于阅读的:&product.Controller{}
  • 控制器处理器总是导出的。所有不是处理器的函数必须是非导出的
  • CRUD 操作命名和路由:
方法URI处理器名称描述
GET/productIndex()获取产品列表
POST/productCreate()创建产品
GET/product/{id}Show()显示产品
PATCH/product/{id}Update()更新产品
PUT/product/{id}Upsert()创建或替换产品
DELETE/product/{id}Delete()删除产品

依赖注入

有两种方法可以将依赖项注入控制器。对于这两种方法,建议使用接口来定义依赖项,而不是服务类型本身。这消除了直接的代码依赖,并有助于编写测试。

使用服务容器

此方法非常适合与 controller.Init()controller.RegisterRoutes()router.Controller() 一起使用。控制器将从框架提供的服务容器中获取其依赖项。

go
// http/controller/product/product.go
type Controller struct {
	goyave.Component
	ProductService Service
}

func (ctrl *Controller) Init(server *goyave.Server) {
	ctrl.ProductService = server.Service(service.Product).(Service)
	ctrl.Component.Init(server)
}
go
// http/route/route.go
func Register(server *goyave.Server, router *goyave.Router) {
	router.Controller(&product.Controller{})
}

TIP

  • 注意我们使用接口进行类型断言,而不是实际的服务类型。
  • 了解更多关于服务以及如何注册它们的信息,请参阅专用部分

使用构造函数

如果您不想使用 controller.Init()router.Controller(),并且您更喜欢在主路由注册器内部注册路由,而不是在控制器旁边,为您的控制器定义一个构造函数,该函数将其所有依赖项作为参数:

go
// http/controller/product/product.go
type Controller struct {
	goyave.Component
	ProductService Service
}

func NewController(server *goyave.Server, productService Service) *Controller {
	ctrl := &Controller{
		ProductService: productService,
	}
	ctrl.Init(server)
	return ctrl
}
go
// http/route/route.go
func Register(server *goyave.Server, router *goyave.Router) {
	//...
	{
		ctrl := product.NewController(server, productService)
		productRouter := router.Subrouter("/products")
		productRouter.Get("/{productID:[0-9+]}", ctrl.Show)
		//...
	}
}