Bu örnekte, Golang'da HTTP sunucusunun temel bağlamı (context) olarak signal.NotifyContext bağlamını kullanacağız. Bunu yapmamızın nedeni, kesinti sinyallerini tüm canlı HTTP isteklerine yaymak, böylece bunların sonlandırılmasını veya çalıştırılmasının engellenmesini istememizdir. Her istek bağlamı bu bağlamı dinleyecektir. Bağlam, orijinal son tarihi beklenmeden derhal iptal edilecektir; dolayısıyla bu, bir bağlamdaki tüm zaman aşımlarına/son teslim tarihlerine göre önceliklidir.


Bonus özellik olarak, aynı bağlamı kullanarak sunucuyu zarif bir şekilde kapatacağız. Kapatma, signal.NotifyContext'ten bağlam bekler ve sunucunun sorunsuz bir şekilde kapatılmasını başlatmak için onu dinler. Gelen içerik kapatılır kapatılmaz, sunucunun otomatik olarak kapatılması başlar ancak zaman aşımı süresi verilen canlı isteklerin tamamlanmasına izin verir.


Örnek


İşte temel bir örnek.


main.go


package main

import (
"context"
"log/slog"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"

"random/api"
"random/service"
)

func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

// HTTP router -------------------------------------------------------------
rtr := http.NewServeMux()
rtr.HandleFunc("/fast", api.Fast)
rtr.HandleFunc("/slow", api.Slow)

// HTTP server -------------------------------------------------------------
srv := &http.Server{
// BaseContext accepts context from `signal.NotifyContext` and propagates
// interruption signals to all live HTTP requests so that they are either
// terminated or prevented from being run. Every request context listens
// on this context. The context will immediatelly be cancelled without
// waiting for its original deadline so this takes precedence over all
// timeouts/deadlines on a context.
BaseContext: func(_ net.Listener) context.Context { return ctx },
Handler: rtr,
Addr: ":1234",
}

// Service -----------------------------------------------------------------
svc := service.Service{
Server: srv,
Timeout: time.Second * 10,
}

go func() {
if err := svc.Start(); err != nil {
slog.Error("app start", "error", err)

return
}
}()

if err := svc.Shutdown(ctx); err != nil {
slog.Warn("dirty service shutdown with possible interruptions", "error", err)
} else {
slog.Info("clean service shutdown without any interruption")
}
}

service.go


package service

import (
"context"
"net/http"
"time"
)

type Service struct {
Server *http.Server
Timeout time.Duration
}

func (s Service) Start() error {
if err := s.Server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return err
}

return nil
}

// Shutdown expects context from `signal.NotifyContext` and listens on it to
// start graceful server shutdown. As soon as the incoming context is closed,
// graceful server shutdown starts but allows live requests given timeout duration
// to complete.
func (s Service) Shutdown(ctx context.Context) error {
<-ctx.Done()

ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
defer cancel()

if err := s.Server.Shutdown(ctx); err != nil {
return err
}

return nil
}

api.go


package api

import (
"log/slog"
"net/http"
"time"
)

func Fast(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte(`fast`))
}

func Slow(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

for {
time.Sleep(time.Millisecond * 100)

go func() {
select {
case <-ctx.Done():
slog.Info(ctx.Err().Error(), slog.Int("routine", 1))
default:
slog.Info("all good", slog.Int("routine", 1))
}
}()
}
}

Test


Hizmeti bir terminal sekmesinde çalıştırın ve /slow uç noktasına bir HTTP isteği gönderin, Ctrl+C tuşuna basın ve ardından günlükleri inceleyin.