Организация сервера на Go с использованием context

Есть такой простой сервер:


import (
    "context"
    "fmt"
    "math/rand"
    "net/http"
    "time"
)

func middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) //Таймаут на 5 секунд
        defer cancel()

        r = r.WithContext(ctx)
        next.ServeHTTP(w, r)
    })
}

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    ch := make(chan string) //Для приёма ответа
    go doSomeLong(ch)
    select {
    case <-ctx.Done(): //Если таймаут превышен
        err := ctx.Err()
        fmt.Println(err)
        fmt.Fprint(w, "Превышен таймаут")
    case str := <-ch: //Если всё нормально, то отправка клиенту сообщения из канала
        fmt.Fprint(w, str)
    }
}

/*Функция для симуляции долгого процесса*/
func doSomeLong(a chan string) {
    sl := rand.Intn(10)
    time.Sleep(time.Duration(sl) * time.Second)
    a <- "Привет!"
}

func main() {
    http.HandleFunc("/", handler)
    fmt.Println("SERVER START AT :8080")
    http.ListenAndServe(":8080", middleware(http.DefaultServeMux))
}

Нормальна такая конструкция в каждом обработчике? Как рендерить готовые файлы?

    select {
    case <-ctx.Done(): //Если таймаут превышен
        err := ctx.Err()
        fmt.Println(err)
        fmt.Fprint(w, "Превышен таймаут")
    case str := <-ch: //Если всё нормально, то отправка клиенту сообщения из канала
        fmt.Fprint(w, str)
    }

Ответы (1 шт):

Автор решения: Pak Uula

Я бы на вашем месте вынес обратку контекста в функцию doSomeLong. Если вы завершите handler по контексту, а горутина останется, то потенциально это может привести к утечке памяти: получателя данных уже нет, а поставщик продолжает их генерировать.

Есть два сценария:

  1. передать контекст в doSomeLong и пусть эта функция сама отслеживает момент прекращения работы.
     var ErrCancelled = errors.New("operation cancelled")
    
     func doSomeLong(ctx context.Context) (string, error) {
         sl := rand.Intn(10)
         select {
         case <-time.After(time.Duration(sl) * time.Second):
             return "Привет!", nil
         case <-ctx.Done():
             return "", ErrCancelled
         }
     }
    
     func handler(w http.ResponseWriter, r *http.Request) {
         ctx := r.Context()
         str, err := doSomeLong(ctx)
         if err != nil {
             if errors.Is(err, ErrCancelled) {
                 w.WriteHeader(http.StatusRequestTimeout)
             } else {
                 w.WriteHeader(http.StatusInternalServerError)
             }
             return
         }
         fmt.Fprint(w, str)
     }
    
  2. План Б: обратать завершение контекста в обработчике HTTP, но как-то уведомить горутину, что время вышло, и прибрать ресурсы. Типичный шаблон - закрыть канал ch по завершению функции handler. Он одновременно решает обе задачи: и ресурсы приберёт, и уведомит горутину о завершении - при записи в закрытый канал горутина запаникует.
    ch := make(chan string) //Для приёма ответа
    defer close(ch)
    
     func doSomeLong(a chan string) {
         defer func() {
             if err := recover(); err != nil {
                 log.Println("doSomeLong filed: ", err)
             }
         }()
         sl := rand.Intn(10)
         time.Sleep(time.Duration(sl) * time.Second)
         a <- "Привет!"
     }
    
    Здесь в отложенной функции вызывается recover, чтобы в рантайме Го не писал сообщения о панике в горутине. Ошибка плановая, исключение поймано и спрятано, всё чинно-благородно.

Как-то так.

Вопрос про "готовые файлы" не понял. Вы спрашивали, как обрабатывать статический контент? Что-то вроде такого:

    fs := http.FileServer(http.Dir("./static"))
    http.Handle("/static/", fs)

Хэндлер http.FileServer умеет обрабатывать запросы к файлам. Тип http.Dir реализует интерфейс http.FileSystem, который использует FileServer для доступа к файлам.

→ Ссылка