Graceful shutdown with context.Context

Before the code: what graceful shutdown actually means

Picture a restaurant at closing time. The good waiter does not yell stop and dump plates on the floor. They stop seating new guests, let the people already eating finish, then turn the lights off. That is graceful shutdown for a server. When the operating system sends a stop signal, you want the server to stop accepting new connections, let the writes already in flight finish, then quit. In Go you wire this with a cancellable context and a WaitGroup that knows how many handlers are still running.

Stop accepting, drain, exit

A clean shutdown has three phases: stop accepting new work, let in-flight work finish, then exit the process.

Quiz: Quiz

Loading practice…

Why is this Go's step 9? Because the goroutine + netpoll model already gives us what Python had to engineer with selectors. The remaining production gap is shutdown. When SIGTERM arrives, we want to drain cleanly, not drop connections mid-write.

09-graceful-shutdown/main.go
go
func main() {
    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer stop()

    listener, _ := net.Listen("tcp", addr)
    log.Printf("server listening on %s", addr)
    var wg sync.WaitGroup

    go func() {
        for {
            conn, err := listener.Accept()
            if err != nil { return }
            wg.Add(1)
            go func(c net.Conn) {
                defer wg.Done()
                handleConn(ctx, c)
            }(conn)
        }
    }()

    <-ctx.Done()
    listener.Close()
    // wait for handlers with 5s timeout
}

signal.NotifyContext turns SIGINT/SIGTERM into a cancelled context. Wait on it in main.

09-graceful-shutdown/main.go
go
func handleConn(ctx context.Context, conn net.Conn) {
    defer conn.Close()
    r := bufio.NewReader(conn)

    go func() {
        <-ctx.Done()
        conn.SetReadDeadline(time.Now()) // unblocks Read with timeout error
    }()

    for {
        frame, err := parseRESP(r)
        if err != nil { return }
        // ... dispatch
        select {
        case <-ctx.Done(): return // cooperative checkpoint between commands
        default:
        }
    }
}

A second goroutine watches ctx.Done() and calls SetReadDeadline(time.Now()) to unblock any in-flight Read.

SetReadDeadline lets in-flight Reads finish their current chunk cleanly. Close would unblock with an abrupt close error and prevent us from sending a polite final response. The deadline approach is cooperative; close is forced.