DEV Community

Cover image for Graceful Server In Golang
Michael Stevan Lapandio
Michael Stevan Lapandio

Posted on

Graceful Server In Golang

In this post, we'll walk through how to set up a graceful HTTP server in Golang that handles three crucial life cycle stages: Prestart, Start, and Shutdown. We'll also discuss why handling these life cycles gracefully is important.

Introduction
When building servers, it's vital to manage the server lifecycle effectively to ensure reliability and uptime. Graceful shutdowns, in particular, allow your server to finish processing ongoing requests before shutting down, which is critical in production environments.

Server life cycle diagram

We will focus on the "Start" step first.

  • Start
package main

import (
    "fmt"
    "io"
    "net"
    "net/http"
)

var port = "8080"

type gracefulServer struct {
    httpServer *http.Server
    listener   net.Listener
}

func (server *gracefulServer) start() error {
    listener, err := net.Listen("tcp", server.httpServer.Addr)
    if err != nil {
        return err
    }

    server.listener = listener
    go server.httpServer.Serve(server.listener)
    fmt.Println("Server now listening on " + server.httpServer.Addr)
    return nil
}

func handle(w http.ResponseWriter, r *http.Request) {
    io.WriteString(w, "Hello student!\n")
}

// Initialize a new server instance and return reference.
func newServer(port string) *gracefulServer {
    mux := http.NewServeMux()
    mux.HandleFunc("/", handle)
    httpServer := &http.Server{Addr: ":" + port, Handler: mux}
    return &gracefulServer{httpServer: httpServer}
}

func main() {
  server := newServer(port)
  server.start()
}
Enter fullscreen mode Exit fullscreen mode

We’ve added a start() method on our server type. This allows the instance of gracefulServer returned by our newServer method to have a start life cycle method called on it. We essentially create a listener that can receive TCP communication, and then we spawn a goroutine to monitor incoming requests and serve our response to them. If we run the code given above, we should see that the server starts on port 8080 and prints the same to the output console.

Well, our listening is happening in a goroutine! The listener has been detached from the main thread. So, technically, when we run start() in our main function, the execution ends and the program quits, closing all open subprocesses.

To solve this we will need to implement a wait and shutdown properly.

  • Shutdown
package main

import (
    "flag"
    "io"
    "log"
    "net"
    "net/http"
    "os"
    "os/signal"
    "syscall"
)

type gracefulServer struct {
    httpServer *http.Server
    listener   net.Listener
}

func (server *gracefulServer) start() error {
    listener, err := net.Listen(
        "tcp",
        server.httpServer.Addr,
    )
    if err != nil {
        return err
    }

    server.listener = listener
    go server.httpServer.Serve(server.listener)
    log.Default().Printf("Server now listening on %s\n", server.httpServer.Addr)
    return nil
}

func (s *gracefulServer) shutdown() error {
    if s.listener != nil {
        err := s.listener.Close()
        s.listener = nil
        if err != nil {
            return err
        }
    }

    log.Default().Println("Shutting down server")
    return nil
}

func handle(w http.ResponseWriter, r *http.Request) {
    io.WriteString(w, "Hello student!\n")
}

func newServer(port string) *gracefulServer {
    mux := http.NewServeMux()
    mux.HandleFunc("/", handle)
    httpServer := &http.Server{Addr: ":" + port, Handler: mux}
    return &gracefulServer{httpServer: httpServer}
}

func main() {

    // This is how you would get command line arguments
    // passed to your binary at runtime.
    // The last parameter in StringVar is actually a 
    // usage help text in case someone messes up.
    var port string
    flag.StringVar(&port, "port", "8080", "./server -port 8080")
    flag.Parse()

    done := make(chan bool, 1)

    // New channel for OS signals
    interrupts := make(chan os.Signal, 1)
    // Set up the channel to be notified on the following signals
    signal.Notify(interrupts, syscall.SIGINT, syscall.SIGTERM)

    server := newServer(port)
    err := server.start()
    if err != nil {
        log.Fatalf("Error starting server - %v\n", err)
    }

    // Goroutine to set up the shutdown flow
    go func() {
        sig := <-interrupts
        log.Default().Printf("Signal intercepted - %v\n", sig)
        server.shutdown()
        done <- true
    }()

    // Wait for the shutdown flow to send true
    <-done
}
Enter fullscreen mode Exit fullscreen mode

As you can see we added a new channel to receive one OS signal called interrupts. Next we set up the channel to be notified whenever our OS emits SIGTERM or SIGINT. We created a goroutine to listen to the interrupts channel and gracefully call shutdown. After the shutdown is completed, we emit a boolean to our done channel, allowing the main thread to proceed with the execution, therefore bringing our server life cycle to an elegant close.

  • Prestart
package main

import (
    "io"
    "log"
    "net"
    "net/http"
    "os"
    "os/signal"
    "syscall"
)

var welcomeMsg = "Welcome to the graceful server! 💃🏼\n"

type gracefulServer struct {
    httpServer *http.Server
    listener   net.Listener
}

func withSimpleLogger(handler http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Default().Printf("Incoming traffic on route %s", r.URL.Path)
        handler.ServeHTTP(w, r)
    })
}

func (server *gracefulServer) preStart() {
    server.httpServer.Handler = withSimpleLogger(server.httpServer.Handler)
}

func (server *gracefulServer) start() error {
    listener, err := net.Listen(
        "tcp",
        server.httpServer.Addr,
    )
    if err != nil {
        return err
    }

    server.listener = listener
    go server.httpServer.Serve(server.listener)
    log.Default().Printf("Server now listening on %s\n", server.httpServer.Addr)
    return nil
}

func (s *gracefulServer) shutdown() error {
    if s.listener != nil {
        err := s.listener.Close()
        s.listener = nil
        if err != nil {
            return err
        }
    }

    log.Default().Println("Shutting down server")
    return nil
}

func handle(w http.ResponseWriter, r *http.Request) {
    io.WriteString(w, "Hello student!\n")
}

func greetingHandler(w http.ResponseWriter, r *http.Request) {
    io.WriteString(w, welcomeMsg)
}

func newServer(port string) *gracefulServer {
    mux := http.NewServeMux()
    mux.HandleFunc("/", handle)
    mux.HandleFunc("/greeting", greetingHandler)

    httpServer := &http.Server{Addr: ":" + port, Handler: mux}
    return &gracefulServer{httpServer: httpServer}
}

func main() {

    port := "8080"
    done := make(chan bool, 1)
    interrupts := make(chan os.Signal, 1)
    signal.Notify(interrupts, syscall.SIGINT, syscall.SIGTERM)

    server := newServer(port)
    server.preStart()
    err := server.start()
    if err != nil {
        log.Fatalf("Error starting server - %v\n", err)
    }

    go func() {
        sig := <-interrupts
        log.Default().Printf("Signal intercepted - %v\n", sig)
        server.shutdown()
        done <- true
    }()

    <-done
}
Enter fullscreen mode Exit fullscreen mode

We added a preStart() method to gracefulServer, just like our other life cycle methods. This one modifies the server’s handler to add another middleware that logs the path of all incoming traffic. Then we invoke this method in our main function before we invoke start(). And there we go! We have successfully added a prestart hook to our server and learned how to add middleware to our routes.

A graceful server is not just about elegant shutdowns—it's about building reliable, maintainable, and production-ready applications. The patterns we've implemented provide a solid foundation that scales with your application's growth.

A graceful server doesn't just shut down elegantly—it lives elegantly, handling every phase of its lifecycle with purpose and reliability.

Found this helpful? Share it with your fellow Go developers!

Top comments (0)