#go
#Вперед
Вопрос:
По разным причинам я чувствую, что http.ListenAndServe
это не соответствует моим потребностям.
Мне нужно было иметь возможность определять связанный адрес и порт (т. Е. При использовании ":0"
), Поэтому я ввел net.Listener
, прочитал listener.Addr()
, а затем передал http.Serve(listener, nil)
.
Затем мне нужно было иметь возможность запускать два HTTP-сервера с разными обработчиками URL, поэтому я ввел an http.NewServeMux()
, добавил необходимые mux.HandleFunc("/path", fn)
обработчики и передал как http.Serve(listener, mux)
.
Затем мне нужно было иметь возможность чисто останавливать эти серверы и отключать любые соединения независимо от самой основной программы, так что теперь я представил amp;http.Server{Handler: mux}
, что я могу go func() { server.Serve(listener) }()
.
Теоретически я могу остановить это, позвонив server.Shutdown(ctx)
, но теперь ни один из доступных контекстов, import "context"
похоже, не предлагает того, что я хочу. Я хочу иметь возможность подождать, пока завершится чистое завершение работы, а затем продолжить работу с моим кодом.
Я понимаю, что я должен быть в состоянии <- ctx.Done()
достичь этого, но я пробовал оба context.Background()
, context.TODO()
и ни один из них, похоже, не «срабатывает» ctx.Done()
, и в итоге я блокирую навсегда. Другие context
варианты, похоже, зависят от времени.
Если я чего-то не жду или не передаю nil
, server.Shutdown(ctx)
кажется, что все заканчивается слишком быстро, и я вижу, что на самом деле ничего не закрыто ( runtime.Numgoroutine() != 1
)
Я могу time.Sleep(duration)
для некоторой произвольной продолжительности, но я не хочу произвольной продолжительности. Я хочу знать, что server.Shutdown
все завершилось чисто.
package main
import (
"fmt"
"net"
"net/http"
"runtime"
"time"
)
func main() {
var err error
listener, err := net.Listen("tcp", "localhost:0")
fmt.Printf("Listening on http://%vn", listener.Addr())
mux := http.NewServeMux()
mux.HandleFunc("/", handleIndex)
stop, err := startHTTPServer(listener, mux)
d, _ := time.ParseDuration("5s")
time.Sleep(d) // delay here just for example of "long-running" server
close(stop) // closing the channel returned by my helper should trigger shutdown
time.Sleep(d) // if this delay is here, I see the "Stopped" message
if err != nil {
panic(err)
}
fmt.Printf("End of program, active goroutines: %v", runtime.NumGoroutine())
}
// startHTTPServer is a helper function to start a server and return a channel that can trigger shutdown
func startHTTPServer(listener net.Listener, handler http.Handler) (stop chan struct{}, err error) {
stop = make(chan struct{})
server := amp;http.Server{Handler: handler}
go func() {
fmt.Println("Starting server...")
err = server.Serve(listener)
}()
go func() {
select {
case <-stop:
fmt.Println("Stop channel closed; stopping server...")
err = server.Shutdown(nil) // what is passed instead of nil here?
fmt.Println("Stopped.")
return
}
}()
return
}
func handleIndex(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello world")
}
Я пробовал оба context.Background()
и context.TODO()
. Я пробовал new(context.Context)
, но это бросило SIGSEGV
. Я пробовал nil
, и это вообще не ждет.
Я попытался добавить a sync.WaitGroup
и вызвать wg.Wait()
вместо второго time.Sleep(d)
, но мне все равно нужно дождаться server.Shutdown()
завершения перед вызовом wg.Done()
(и defer wg.Done()
назвал его слишком рано).
Я чувствую, что с контекстами, группами ожидания и т. Д. Я Просто добавляю в код cruft, не понимая, зачем это нужно.
Каков правильный, чистый, идиоматический способ дождаться server.Shutdown
завершения?
Ответ №1:
Чтобы дождаться завершения работы в основной подпрограмме, вызовите Shutdown из этой подпрограммы. Устраните канал и дополнительную подпрограмму.
listener, _ := net.Listen("tcp", "localhost:0")
fmt.Printf("Listening on http://%vn", listener.Addr())
mux := http.NewServeMux()
mux.HandleFunc("/", handleIndex)
server := amp;http.Server{Handler: mux}
go func() {
fmt.Println("Starting server...")
err := server.Serve(listener)
if err != nil amp;amp; err != http.ErrServerClosed {
log.Fatal(err)
}
}()
time.Sleep(5 * time.Second) // delay here just for example of "long-running" server
// Shutdown and wait for server to complete.
server.Shutdown(context.Background())
Если вы хотите ограничить время ожидания завершения работы сервера, context.Background()
замените контекст, созданный с указанием крайнего срока.
Комментарии:
1. Анонимная подпрограмма должна обрабатывать http. Ошибка Serverclosed изящно для чистого завершения работы.
2. Для чего-то без тайм-аута
context.Background()
правильно, нетcontext.TODO()
. Согласно документам: «Код должен использовать контекст. ЗАДАЧА, когда неясно, какой контекст использовать, или он еще недоступен (поскольку окружающая функция еще не была расширена для принятия параметра контекста). »3. @Peter I обновил ответ, чтобы аккуратно обработать ErrServerClosed. Остановка канала и получение подпрограммы на этом канале не требуется.
Ответ №2:
Теоретически я могу остановить это, вызвав server .Завершение работы (ctx), но теперь ни один из доступных контекстов в import «context», похоже, не предлагает того, что я хочу. Я хочу иметь возможность подождать, пока завершится чистое завершение работы, а затем продолжить работу с моим кодом.
Это означает, что вы должны передать контекст, у которого нет тайм-аута. Ни context.Background()
один, ни context.TODO()
тайм-аут и, следовательно, не подходят (действительно, см. Ниже). Используете ли вы тот или иной вариант, зависит от того, планируете ли вы тайм-аут завершения работы (вы должны предотвратить то, чтобы rough client не мешал вам завершить работу вашего сервера) или нет.
Я понимаю, что я должен иметь возможность <- ctx.Done() для достижения этой цели, но я пробовал как context.Background(), так и context .TODO() и ни один из них, похоже, не «запускает» ctx.Done(), и я в конечном итоге блокирую навсегда. Другие параметры контекста, похоже, зависят от времени.
Ну, это неправильно. Ни context.Background()
nor context.TODO()
не закроют их Done
, поскольку у них нет тайм-аута. Но нет необходимости ждать завершения: server.Shutdown
это обычная функция, которая возвращается после того, как сервер действительно выключен должным образом (это то, что вы, кажется, хотите) или время ожидания контекста истекло. В любом случае server.Shutdown
просто возвращает.
Простой
server.Shutdown(context.TODO())
это то, что вы хотите. (На данный момент, в долгосрочной перспективе: передайте контекст, время ожидания которого истекает через долгое, но конечное время.)
Но ваш код в любом случае выглядит подозрительно: ваш функциональный startHTTPServer неправильно обрабатывает ошибки: не ошибка при запуске, а не ошибка при остановке. Если ваш сервер не запустился, вы не можете его остановить, и ваш код просто проглатывает ошибку. Это также пикантно при ошибке. Ваши проблемы, вероятно, не связаны с контекстом, переданным на сервер.Завершение работы, но откуда-то еще.
Проблема в том, что ваш код не ожидает сервер.Завершение работы для возврата, поскольку эта функция запускается в подпрограмме без какой-либо синхронизации с вызывающим startHTTPServer: не делайте этого. Действительно: контекст сервера.Завершение работы не проблема.
Ниже приведен непроверенный код, который немного устраняет проблемы. Он не готов к производству, как вы можете видеть при всех настроениях.
// startHTTPServer is a helper function to start a server and returns
// a channel to stop the server and a channel reporting errors during
// starting/stopping the server or nil if the server was shut down
// properly
func startHTTPServer(listener net.Listener, handler http.Handler) (stop chan bool, problems chan error) {
stop, problems = make(chan bool), make(chan error)
server := amp;http.Server{Handler: handler} // TODO: set timeouts
go func() {
fmt.Println("Starting server...")
err := server.Serve(listener)
if err != http.ErrServerClosed {
problems <- err // TODO: tag/classify as startup error
}
}()
go func() {
select {
case <-stop:
fmt.Println("Stop channel closed; stopping server...")
err := server.Shutdown(context.TODO())
fmt.Println("Stopped.")
problems <- err // TODO: tag/classify as shutdown error
case e := <-problems:
problems <- e // resend, this error is not for us
return // stop waiting for stop as server did not start anyway.
}
}()
return stop, problems
}
Возможны и другие решения, например, возврат отдельных каналов ошибок запуска и завершения работы и т.д.
Комментарии:
1. Спасибо за это — я уверен, что вы правы. Но в этом случае проблема в том, что я действительно не понимаю, куда я должен поместить синхронизацию и как я должен ее вызывать. Можете ли вы предоставить исправленную
startHTTPServer
версию, которая демонстрирует вашу позицию? Я уверен, что есть много ошибок, которые я не обрабатываю, но, похоже, я не могу найти способ изящно обработать их и получитьListenAndServe
эквивалент, который возвращает что-то неопределенно закрываемое, сохраняя при этом простую подпись вызова.2. Для чего-то без тайм-аута
context.Background()
правильно, нетcontext.TODO()
. Согласно документам: «Код должен использовать контекст. ЗАДАЧА, когда неясно, какой контекст использовать, или он еще недоступен (поскольку окружающая функция еще не была расширена для принятия параметра контекста). »3. @Adrian Моим аргументом было: вам, вероятно, не следует использовать контекст, у которого нет тайм-аута, особенно если на вашем сервере нет тайм-аутов. Я предложил TODO, потому что считаю, что бесконечное ожидание опасно : следует добавить — возможно, очень длинный, но конечный — тайм-аут.