Introduction
Think back to nursery school or to Robert Fulghum's "All I Really Need to Know I Learned in Kindergarten" [if you read it] one of the first lessons we learned was simple:
Clean up your own mess
Put things back where you found them
Share everything
As kids, this meant picking up toys after playtime. As engineers, it means making sure our programs don’t leave behind open connections, stuck processes, or unfinished business. In Go web servers, these lessons are more than just good manners, they’re essential for building reliable, maintainable software.
Let’s walk through a Go web server and see how these childhood rules guide our approach to resource cleanup.
The Mess We Make
Every time our server starts, it opens connections to databases, listens for HTTP requests, and spins up goroutines to handle these requests. If we don’t clean up, these resources pile up like toys left on the floor. Eventually, someone trips.
And just like a classroom with toys everywhere, a production system with resource leaks can become a minefield. Outages creep in, and the cause is often hard to spot until someone steps on a stray toy, or a database refuses new connections, or a server freezes on shutdown, forcing a risky manual kill.
Context: The Teacher’s Bell
In nursery school, the teacher rings a bell to signal the end of playtime. In Go, the context
package is our bell. It tells every part of our program when it’s time to stop, clean up, and go home.
Our server listens for signals from the operating system—SIGINT, SIGTERM, SIGQUIT—just like listening for the teacher’s bell. When a signal arrives, we use signal.NotifyContext
to broadcast the message: “Clean up time!”
ctx, cancel := signal.NotifyContext(
context.Background(),
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGQUIT,
)
defer cancel()
This ctx
is passed everywhere: to the database, to HTTP handlers, to background jobs. When it’s cancelled, everyone knows it’s time to wrap up.
With context as our bell, we don’t need to shout across the room or rely on everyone to notice the clock. Every goroutine listens for the same signal [or derives their own signal from it], so the cleanup is coordinated and calm. This makes the code easier to reason about and less prone to subtle bugs.
Cleaning Up the Database Toys
When we connect to the database, we use our context. If the teacher says "Stop!" (the process is killed or our timeout expires), the connection attempt is cancelled. No waiting forever for a broken database to never connect or leaving behind an open connection after the server receives a signal to shutdown.
func ConnectDB(ctx context.Context, dbURL string) (*sql.DB, error) {
db, err := sql.Open("postgres", dbURL)
if err != nil {
return nil, fmt.Errorf("opening database connection: %w", err)
}
timeoutCtx, cancel := context.WithTimeout(ctx, time.Second*30)
defer cancel()
if err := db.PingContext(timeoutCtx); err != nil {
return nil, fmt.Errorf("pinging database: %w", err)
}
return db, nil
}
And when we’re done, we put the connection back where we found it:
conn, err := ConnectDB(ctx, dbURL)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
Just like putting toys back on the shelf, closing the connection ensures the next group can play without running into a mess. And if you ever scale your service horizontally, proper cleanup means new instances can start without waiting for old connections to be released. Rolling deployments and zero-downtime upgrades become smooth transitions, not frantic scrambles.
Sharing Toys: The HTTP Client
Just like we don’t need a new ball for every child, we don’t always need a new http.Client
for every request. We can share one client, so connections are reused and the playground isn’t cluttered with abandoned toys.
This means faster requests, fewer resources wasted, and a happier playground for everyone. And when you want to set timeouts, proxies, or custom transports, you do it once and all requests benefit. Testing and mocking become easier too—just swap out the client for a test double and the whole system adapts. I wrote about mocking HTTP requests here.
func PingURL(ctx context.Context, client *http.Client, url string) error {
// use context here again to make sure our request does not take forever.
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("creating request: %w", err)
}
res, err := client.Do(req)
if err != nil {
return fmt.Errorf("making request: %w", err)
}
defer res.Body.Close()
if res.StatusCode >= http.StatusBadRequest {
b, err := io.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("reading error response: %w", err)
}
return fmt.Errorf("response from %s: %s", url, b)
}
return nil
}
Furthermore, because of the shared context, every HTTP request can have its own timer derived from the parent signal, like saying, "You have 30 seconds to finish your game." If the time runs out, or if the teacher calls for cleanup, the request stops.
handler.HandleFunc("/ping/{url}", func(w http.ResponseWriter, r *http.Request) {
pingCtx, cancel := context.WithTimeout(ctx, time.Second*30)
defer cancel()
url := r.PathValue("url")
if err := PingURL(pingCtx, client, url); err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
w.Write([]byte("OK"))
})
This keeps the playground fair—no one hogs the toys, and everyone gets a turn. And if a third-party API is down, your server won’t hang forever waiting for a response. The bell rings, the game ends, and the playground stays lively and responsive.
The Final Bell: Graceful Shutdown
When playtime is really over, we wait for everyone to finish their last game, but not forever. We give them some time to wrap up, then we close the playground.
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Fatal(err)
}
This way, no one is left behind, and the playground is ready for the next group. Deploying updates or restarting servers doesn’t mean dropped requests or angry users. It’s a professional responsibility, and your ops team will thank you.
Why It Matters
If we don’t clean up, the next group can’t play—the database is full, the server is stuck, the system is a mess, and someone has to stop reading a book to his son at night. By listening for signals, using context, and always putting things back, our Go server stays tidy, reliable, and ready for the next round.
And these patterns aren’t just for big companies. Even a small side project benefits from being easy to restart, scale, and debug.
Conclusion
The lessons from nursery school (clean up your mess, put things back, share everything, listen for the teacher) are the same lessons that keep our web servers healthy. With Go’s context and careful resource management, we make sure our playground is always ready for the next game.
Read the Full Code
Want to see the complete implementation?
Check out the repo: github.com/ekediala/resource_cleanup
Top comments (0)