Одно из моих самых любимых нововведений в недавнем релизе 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 {
// Обработка ошибки
}
Интерфейсы Flusher и Hijacker
Тип 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’ы
Теперь также проще и безопаснее создавать и использовать пользовательские реализации 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".