golang

Для чего нужен тип http.ResponseController?

  • четверг, 25 мая 2023 г. в 00:00:18
https://habr.com/ru/companies/otus/articles/736980/

Одно из моих самых любимых нововведений в недавнем релизе Go 1.20 — это тип http.ResponseController, который может похвастаться тремя очень приятными полезностями:

  1. Теперь вы можете переопределять ваши общесерверные таймауты/дедлайны чтения и записи новыми для каждого отдельного запроса.

  2. Шаблон использования интерфейсов http.Flusher и http.Hijacker стал более понятным и менее сложным. Нам больше не нужны никакие утверждения типов!

  3. Он делает проще и безопаснее создание и использование пользовательских реализаций 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".