Skip to content

响应

介绍

*goyave.Response 实现了 http.ResponseWriter。此包装器控制响应流程,并提供了许多便捷的方法来编写 HTTP 响应。所有写入首先通过 *goyave.Response,即使在使用链式写入器时也是如此。

预写入

PreWrite 是在任何 Write 之前始终调用的操作。因为 HTTP 标头必须在响应正文之前写入,写入将自动将它们发送到客户端。*goyave.Response 使用此钩子确保写入正确的状态码而不是默认的 200 OK。这样,您可以使用 response.Status() 定义状态码,而无需立即发送响应标头。然后,中间件和状态处理器可以读取状态码以处理动态响应,即使标头尚未写入。

写入响应

您可以设置状态而不立即写入响应标头。这与 response.WriteHeader() 不同,后者会写入响应标头。建议使用 response.Status()

go
response.Status(http.StatusUnauthorized)

INFO

第二次调用此方法将无效。

response.JSON() 将给定的任何内容编组为 JSON,自动添加 Content-Type: application/json; charset=utf-8 标头并写入响应。或者,如果简单的字符串足够,response.String() 将为您工作:

go
response.JSON(http.StatusOK, someDTO)
response.String(http.StatusOK, "Hello world")

您可以使用 response.File()(将文件作为“inline”发送)或 response.Download()(将文件作为附件发送)从任何实现 fs.StatFS 的文件系统写入文件。

go
import (
	"embed"
	"goyave.dev/goyave/v5/util/fsutil"
)

//go:embed resources
var resources embed.FS

//...
fs := fsutil.NewEmbed(resources)
response.File(fs, "resources/img/logo.png")
response.Download(fs, "resources/manual.pdf")

您也可以使用 Write() 以标准方式写入响应:

go
response.WriteHeader(http.StatusOK)
_, err := response.Write(bytes)
if err != nil {
	panic(err)
}

INFO

response.JSON()response.String() 相反,response.Write() 可以被多次调用。它可用于将大响应流式传输到客户端,并支持通常的 io 助手,例如 io.Copy()

不支持的功能

因为这不是框架的重点,所以某些罕见用例未被覆盖,例如渲染模板和重定向。因为 *goyave.Response 实现了 http.ResponseWriter,您仍然可以将其用作常规写入器:

go
import (
	"html/template"
	"net/http"

	"goyave.dev/goyave/v5"
	"goyave.dev/goyave/v5/util/errors"
)

//...

// 重定向
http.Redirect(response, request.Request(), "/target", http.StatusPermanentRedirect)

// html 模板
tmpl := template.Must(template.ParseFiles("layout.html"))
data := map[string]any{
	//...
}
err := tmpl.Execute(response, data)
if err != nil {
	response.Error(errors.New(err))
	return
}

错误处理

当错误一直返回到控制器时,您可以使用 response.Error() 优雅地处理它。此方法将记录错误并将响应状态设置为 500 Internal Server Error。如果启用了调试(配置 app.debug)且尚未向响应写入任何内容,错误消息也将写入响应正文。

go
response.Error(err)

WriteDBError() 允许您更轻松地处理数据库错误。它接受一个错误,如果错误是 gorm.ErrRecordNotFound 错误,则自动写入 HTTP 状态码 404 Not Found。如果有其他类型的错误,则调用 Response.Error()。此方法在有错误时返回 true。然后您可以在控制器中安全地 return

go
func (ctrl *ProductController) Show(response *goyave.Response, request *goyave.Request) {
	product := model.Product{}
	result := ctrl.DB().First(&product, request.RouteParams["id"])
	if response.WriteDBError(result.Error) {
		return
	}
	response.JSON(http.StatusOK, product)
}

TIP

专用部分中了解首选的错误处理方法

链式写入器

可以替换 *goyave.Response 使用的 io.Writer。这为操作发送到客户端的数据提供了更大的灵活性。它使压缩响应、将其写入日志等变得更加容易。您可以链接任意数量的写入器。

通常,响应的写入器在中间件中被替换。当前写入器从 *goyave.Response 中获取,并用作新创建的链式写入器的目标。然后将此新链式写入器设置在 *goyave.Response 中。

链式写入器图

链式写入器是组件,就像中间件和控制器一样。它们可以与 goyave.CommonWriter 组合,这是一个实现链式写入器必须实现的所有基本方法的结构:goyave.PreWriterio.Closergoyave.Flusher

请注意,在调用链式写入器的 Write() 方法时,请求标头已经写入。因此,更改标头或状态没有任何效果。如果您想更改标头,请在 PreWrite(b []byte) 函数(来自 goyave.PreWriter 接口)中执行。

写入器在最终化阶段结束时关闭,告诉它们应用程序完全完成了此请求。

以下示例是一个链式写入器及其中间件的简单实现,用于记录客户端发送的所有内容:

go
import (
	"io"

	"goyave.dev/goyave/v5"
	"goyave.dev/goyave/v5/util/errors"
)

type LogWriter struct {
	goyave.CommonWriter
	response *goyave.Response
	body     []byte
}

func NewWriter(response *goyave.Response) *LogWriter {
	return &LogWriter{
		CommonWriter: goyave.NewCommonWriter(response.Writer()),
		response:     response,
	}
}

func (w *LogWriter) Write(b []byte) (int, error) {
	w.body = append(w.body, b...)
	n, err := w.CommonWriter.Write(b)
	return n, errors.New(err)
}

func (w *LogWriter) Close() error {
	w.Logger().Info("RESPONSE", "body", string(w.body))
	return errors.New(w.CommonWriter.Close())
}

type LogMiddleware struct {
	goyave.Component
}

func (m *LogMiddleware) Handle(next goyave.Handler) goyave.Handler {
	return func(response *goyave.Response, request *goyave.Request) {
		logWriter := NewWriter(response)
		response.SetWriter(logWriter)

		next(response, request)
	}
}

您可以覆盖 PreWrite()Close()Flush() 的默认行为。如果这样做,请确保调用 CommonWriter 的实现,以便这些操作的写入器正确链接:

go
func (w *Writer) PreWrite(b []byte) {
	//...
	w.CommonWriter.PreWrite(b)
}

func (w *Writer) Close() error {
	//...
	return errors.New(w.CommonWriter.Close())
}

func (w *Writer) Flush() error {
	//...
	return errors.New(w.CommonWriter.Flush())
}

INFO

链式写入器支持其底层写入器的 http.Flushergoyave.Flusher。这些接口之间的唯一区别是 http.FlusherClose() 方法不返回 error,但 goyave.Flusher 的返回。

劫持

Goyave 响应实现了 http.Hijacker。劫持是接管当前 HTTP 请求连接的过程。这可以用于例如 websockets

在控制器处理器之后执行的中间件以及状态处理器在连接被劫持后继续正常工作。调用者应正确设置响应状态以确保中间件和状态处理器正确执行。通常,Hijack 方法的调用者将 HTTP 状态设置为 http.StatusSwitchingProtocols。如果未设置状态,将保持常规行为,并将 204 No Content 设置为响应状态。

劫持后写入 *goyave.Response 应避免,因为它会导致错误。因此,建议在控制器处理器之后执行的中间件中写入任何内容之前始终检查连接是否被劫持(使用 Response.Hijacked())。劫持后调用 response.Status() 是安全的,但不会有任何效果。

调用 Hijack 后,不得使用原始请求正文读取器。原始请求的 context.Context 保持有效,并且在请求生命周期结束之前不会被取消。

链式写入器在连接被劫持时不会被丢弃,并且仍将在请求生命周期结束时照常关闭。