Skip to Content
Course content

51: Building an HTTP Server with net/http

Click on the "Edit" button in the top corner of the screen to edit your slide content.

By now, you've got the hang of Go's types and concurrency. Now we get to the part where your code actually talks to the world. When you first look at the net/http package, you'll see a way to get a server running in about five lines of code. It's tempting. I'll show you that way first, and then I'll explain why I almost never use it in a production environment.

The lure of the default multiplexer

If you're building a quick prototype, you'll likely reach for http.HandleFunc and http.ListenAndServe. Let's imagine we're building a simple status API for a service that monitors database connectivity. The "naive" version looks like this:

package main

import (
	"fmt"
	"net/http"
)

func statusHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "System Operational")
}

func main() {
	http.HandleFunc("/status", statusHandler)
	http.ListenAndServe(":8080", nil)
}

It's clean, right? No boilerplate. But look closely at that nil passed to ListenAndServe. That tells Go to use the DefaultServeMux. This is a global variable shared by your entire program. I've seen this cause absolute chaos in larger projects where different packages were accidentally registering routes to the same global multiplexer, leading to "route hijacking" that was a nightmare to debug.

Where the default server breaks

The bigger issue isn't actually the routing; it's the server configuration. http.ListenAndServe is a wrapper that creates an http.Server with default settings. The problem is that the defaults are far too permissive for the open internet. Specifically, there are no timeouts. No ReadTimeout, no WriteTimeout, no IdleTimeout.

In a real-world scenario, a malicious client (or just a very buggy one) could open a connection to your server and just... stop. They send half a request and hang. Because you're using the default server, that connection stays open indefinitely. Do this a few thousand times, and you've effectively DDOS'd yourself by exhausting your file descriptors. It's a classic "it worked in dev" failure that crashes in production.

Taking control with a custom server

The professional way to do this is to be explicit. You want your own ServeMux to avoid global state, and you want a configured http.Server struct so you can tell Go exactly when to give up on a slow connection. Here is how I would write that same status API:

package main

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

func statusHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "System Operational")
}

func main() {
	// Create a private multiplexer
	mux := http.NewServeMux()
	mux.HandleFunc("/status", statusHandler)

	// Configure the server explicitly
	srv := &http.Server{
		Addr:         ":8080",
		Handler:      mux,
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
		IdleTimeout:   120 * time.Second,
	}

	fmt.Println("Server starting on :8080...")
	if err := srv.ListenAndServe(); err != nil {
		fmt.Printf("Server failed: %s\n", err)
	}
}

Now, the trade-off is a bit more typing up front, but you've gained total control. If a client takes more than five seconds to send the request headers, the server kills the connection. Your app is now resilient. I also moved the routing into mux, meaning if you ever want to write a unit test for your routing logic, you can pass that mux into a test recorder without actually having to spin up a network socket.




📋 Practical Task

Build a Read-Only Project Metadata API

Your task is to create a production-ready HTTP server that exposes project metadata. Instead of using the global DefaultServeMux, you must implement a custom ServeMux and a configured http.Server.

Requirements:

  • Create a route /metadata that returns a plain text string containing a project name and version (e.g., "Project: Nexus, Version: 1.2.0").
  • Create a route /health that returns "OK".
  • Set a ReadTimeout of 2 seconds and a WriteTimeout of 5 seconds.
  • The server should listen on port 9000.
  • Ensure the server is started using the srv.ListenAndServe() method rather than the package-level http.ListenAndServe.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.