Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
51: Building an HTTP Server with net/http
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
/metadatathat returns a plain text string containing a project name and version (e.g., "Project: Nexus, Version: 1.2.0"). - Create a route
/healththat returns "OK". - Set a
ReadTimeoutof 2 seconds and aWriteTimeoutof 5 seconds. - The server should listen on port
9000. - Ensure the server is started using the
srv.ListenAndServe()method rather than the package-levelhttp.ListenAndServe.
There are no comments for now.