Для чего нужен тип http.ResponseController?
- четверг, 25 мая 2023 г. в 00:00:18
Одно из моих самых любимых нововведений в недавнем релизе Go 1.20 — это тип http.ResponseController, который может похвастаться тремя очень приятными полезностями:
Теперь вы можете переопределять ваши общесерверные таймауты/дедлайны чтения и записи новыми для каждого отдельного запроса.
Шаблон использования интерфейсов http.Flusher и http.Hijacker стал более понятным и менее сложным. Нам больше не нужны никакие утверждения типов!
Он делает проще и безопаснее создание и использование пользовательских реализаций http.ResponseWriter.
Первые два преимущества упоминаются в описании изменений, которое прилагается к релизу, а третье, кажется, ускользнуло из всеобщего поля зрения... а жаль, потому что оно очень полезное!
Что ж, давайте взглянем на них поближе.
http.Server Go имеет настройки ReadTimeout
и WriteTimeout
, которые вы можете использовать для автоматического закрытия HTTP-соединения, если время, затраченное на чтение запроса или запись ответа, превышает какое-либо фиксированное значение. Эти настройки являются общесерверными и применяются ко всем запросам, независимо от обработчика или URL.
С появлением http.ResponseController
вы теперь можете использовать методы SetReadDeadline() и SetWriteDeadline(), чтобы ослабить или, наоборот, ужесточить эти настройки для каждого конкретного запроса в зависимости от ваших потребностей. Например:
func exampleHandler(w http.ResponseWriter, r *http.Request) {
rc := http.NewResponseController(w)
// Установим таймаут записи в 5 секунд.
err := rc.SetWriteDeadline(time.Now().Add(5 * time.Second))
if err != nil {
// Обработка ошибки
}
// Делаем здесь что-нибудь...
// Записываем ответ как обычно
w.Write([]byte("Done!"))
}
Это особенно полезно для приложений, содержащих небольшое количество обработчиков, которым требуются более длительные таймауты, чем всем остальным, для таких вещей, как обработка загрузки файла или выполнение длительной операции.
Несколько деталей, о которых стоит упомянуть:
Если вы установите очень короткий общесерверный таймаут, и этот таймаут будет достигнут до того, как вы вызовете SetWriteDeadline()
или SetReadDeadline()
, то они не возымеют никакого эффекта. Общесерверный таймаут в этом случае побеждает.
Если ваш базовый http.ResponseWriter
не поддерживает установку таймаутов для отдельных запросов, то вызов SetWriteDeadline()
или SetReadDeadline()
вернет ошибку http.ErrNotSupported
.
Теперь вы можете отменять общесерверный таймаут для отдельных запросов, передав обнуленную структур time.Time в SetWriteDeadlin()
или SetReadDeadline()
. Например:
rc := http.NewResponseController(w)
err := rc.SetWriteDeadline(time.Time{})
if err != nil {
// Обработка ошибки
}
Тип http.ResponseController
также делает более удобным использование «опциональных» интерфейсов http.Flusher и http.Hijacker. Например, до Go 1.20, чтобы отправить данные ответа клиенту, вы могли использовать кода следующего вида:
func exampleHandler(w http.ResponseWriter, r *http.Request) {
f, ok := w.(http.Flusher)
if !ok {
// Обработка ошибки
}
for i := 0; i < 5; i++ {
fmt.Fprintf(w, "Write %d\n", i)
f.Flush()
time.Sleep(time.Second)
}
}
Теперь вы можете сделать это так:
func exampleHandler(w http.ResponseWriter, r *http.Request) {
rc := http.NewResponseController(w)
for i := 0; i < 5; i++ {
fmt.Fprintf(w, "Write %d\n", i)
err := rc.Flush()
if err != nil {
// Обработка ошибки
}
time.Sleep(time.Second)
}
}
Шаблонный код перехвата (hijacking) соединения аналогичен:
func (app *application) home(w http.ResponseWriter, r *http.Request) {
rc := http.NewResponseController(w)
conn, bufrw, err := rc.Hijack()
if err != nil {
// Обработка ошибки
}
defer conn.Close()
// Делаем здесь что-нибудь...
}
Опять же, если ваш базовый http.ResponseWriter
не поддерживает flush или перехват соединения, то вызов Flush()
или Hijack()
в http.ResponseController
также вернет ошибку http.ErrNotSupported
.
Теперь также проще и безопаснее создавать и использовать пользовательские реализации http.ResponseWriter
, которые поддерживают flush и перехват соединения.
Вероятно, проще всего объяснить, как это работает, на примере, поэтому давайте посмотрим на код пользовательской реализации http.ResponseWriter
, которая записывает код состояния HTTP ответа.
type statusResponseWriter struct {
http.ResponseWriter // Встраиваем a http.ResponseWriter
statusCode int
headerWritten bool
}
func newstatusResponseWriter(w http.ResponseWriter) *statusResponseWriter {
return &statusResponseWriter{
ResponseWriter: w,
statusCode: http.StatusOK,
}
}
func (mw *statusResponseWriter) WriteHeader(statusCode int) {
mw.ResponseWriter.WriteHeader(statusCode)
if !mw.headerWritten {
mw.statusCode = statusCode
mw.headerWritten = true
}
}
func (mw *statusResponseWriter) Write(b []byte) (int, error) {
mw.headerWritten = true
return mw.ResponseWriter.Write(b)
}
func (mw *statusResponseWriter) Unwrap() http.ResponseWriter {
return mw.ResponseWriter
}
Итак, здесь мы определили пользовательский тип statusResponseWriter
, который встраивает уже существующий тип http.ResponseWriter
и реализует пользовательские методы WriteHeader()
и Write()
для записи кода состояния HTTP ответа.
Но на что здесь стоит обратить внимание, так это на метод Unwrap()
в конце, который возвращает исходный встроенный http.ResponseWriter
.
Когда вы используете новый тип http.ResponseController
, чтобы сделать flush
, перехватить соединение или установить таймаут, он вызовет этот метод Unwrap()
, чтобы получить доступа к исходному http.ResponseWriter
. При необходимости это делается рекурсивно, поэтому вы потенциально можете наслаивать несколько пользовательских реализации http.ResponseWriter
друг на друга.
Давайте рассмотрим полный пример, где мы используем этот statusResponseWriter
в сочетании с некоторым middleware для логирования кодов состояния ответа, а также двумя обработчиками, один из которых отправляет «нормальный» ответ, а другой – задействует новый тип http.ResponseController
, чтобы сделать flush
.
package main
import (
"log"
"net/http"
"time"
)
type statusResponseWriter struct {
http.ResponseWriter // Встраиваем a http.ResponseWriter
statusCode int
headerWritten bool
}
func newstatusResponseWriter(w http.ResponseWriter) *statusResponseWriter {
return &statusResponseWriter{
ResponseWriter: w,
statusCode: http.StatusOK,
}
}
func (mw *statusResponseWriter) WriteHeader(statusCode int) {
mw.ResponseWriter.WriteHeader(statusCode)
if !mw.headerWritten {
mw.statusCode = statusCode
mw.headerWritten = true
}
}
func (mw *statusResponseWriter) Write(b []byte) (int, error) {
mw.headerWritten = true
return mw.ResponseWriter.Write(b)
}
func (mw *statusResponseWriter) Unwrap() http.ResponseWriter {
return mw.ResponseWriter
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/normal", normalHandler)
mux.HandleFunc("/flushed", flushedHandler)
log.Print("Listening...")
err := http.ListenAndServe(":3000", logResponse(mux))
if err != nil {
log.Fatal(err)
}
}
func logResponse(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sw := newstatusResponseWriter(w)
next.ServeHTTP(sw, r)
log.Printf("%s %s: status %d\n", r.Method, r.URL.Path, sw.statusCode)
})
}
func normalHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusTeapot)
w.Write([]byte("OK"))
}
func flushedHandler(w http.ResponseWriter, r *http.Request) {
rc := http.NewResponseController(w)
w.Write([]byte("Write A...."))
err := rc.Flush()
if err != nil {
log.Println(err)
return
}
time.Sleep(time.Second)
w.Write([]byte("Write B...."))
err = rc.Flush()
if err != nil {
log.Println(err)
}
}
Если хотите, вы можете запустить этот код и попробовать отправить запросы к конечным точкам /normal
и /flushed
:
$ curl http://localhost:3000/normal
OK
$ curl --no-buffer http://localhost:3000/flushed
Write A....Write B....
Вы должны увидеть ответ от flushedHandler
в двух частях, где первая часть – Write A..., и через секунду вторая часть Write B....
И вы также должны увидеть, что statusResponseWriter
и middleware logResponse
успешно составили лог, где будут корректные коды состояния HTTP для каждого ответа.
$ go run main.go
2023/03/06 21:41:21 Listening...
2023/03/06 21:41:32 GET /normal: status 418
2023/03/06 21:41:44 GET /flushed: status 200
Если вам понравилась эта статья, вы можете ознакомиться с моим списком рекомендуемых туториаловов или почитать мои книги Let's Go и Let's Go Further, которые научат вас всему, что вам нужно знать о том, как создавать профессиональные готовые к работе в производственной среде веб-приложения и API-интерфейсы с помощью Go.
Материал подготовлен в преддверии старта онлайн-курса "Golang Developer. Professional".