Организация сервера на 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
по контексту, а горутина останется, то потенциально это может привести к утечке памяти: получателя данных уже нет, а поставщик продолжает их генерировать.
Есть два сценария:
- передать контекст в
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) }
- План Б: обратать завершение контекста в обработчике 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
для доступа к файлам.