For the past two years, the Utah Go User Group has been working through one simple idea: what if we just taught the Go Standard Library, one package at a time? It sounds almost too basic, but it turns out to be one of the best meetup formats I’ve ever seen. Most developers — whether they’ve been writing Go for six months or thirty years — don’t actually know what’s in the standard library. People just know what they import and what they write. But unless you’ve contributed to the Go project itself, the reality is that there are huge corners of the standard library you’ve never touched.
This series has been fantastic because it meets everybody where they are. A brand-new Go developer can learn something immediately useful, and a seasoned engineer can discover APIs they had no idea existed. And as an organizer, you never run into the dreaded “I want to present, but I don’t know what topic to pick.” Every meetup maintains a running list of unclaimed standard library packages. No arguing. No guesswork. Just pick one and teach it.
Nobody had touched the net/http package yet which is wild, because it’s probably the most used package in the entire Go ecosystem. Not because everyone is building web servers, but because everything underneath us is already written that way. Kubernetes is written in Go. Half the cloud-native stack is written in Go. And so much of that is ultimately HTTP traffic moving through Go’s built-in server.
So I decided to take it. And that talk — Beyond Hello World: Mastering net/http for Production Go Services — is now available for anyone who prefers to watch the video or read the written version. My goal with this post is to make the entire talk accessible to readers who want to dig into the ideas at their own pace.
Watch the Talk - Beyond Hello World: Mastering net/http for Production Go Services
*Utah Go User Group – November 2025
Here are the slides* https://github.com/Soypete/talks/tree/main/go-meetup-nethttp
If you prefer learning through video or audio, the talk walks through everything included in this post and more. This article is here for the people who want narrative, depth, and a reference they can return to later.
Why the Standard Library Matters More Than You Think
Every time I teach Go, people tell me the same thing: “I use framework X” or “I just grabbed this router because that’s what everyone else uses.” There’s nothing wrong with frameworks. I gave a course with O’Reilly about Web Servers that focused on some good ones, but I think we forget how much work the standard library is already doing for us.
The standard library is stable. It’s maintained. It’s designed for clarity, predictability, and composability. And every layer of tooling in the Go ecosystem from Kubernetes to internal microservices leans heavily on what’s already there.
The irony is that net/http is both the most used and the least understood package beginners encounter. Almost everyone can write:
http.ListenAndServe(":8080", nil)That’s the “Hello World” of writing services. The next steps go beyond that single line, is to understand how the http.Server works or how handlers really interact with context and timeouts and connection lifecycles, you enter a different tier of Go engineering. And you don’t need anything beyond the standard library to get there.
Understanding the HTTP Server: More Than a Port Number
One of the central points from the talk is that the http.Server struct is not something you should ignore. If you’re running production services, you want to configure your server the same way you configure any other infrastructure component.
s := &http.Server{
Addr: ":8080",
Handler: myHandler,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())source https://pkg.go.dev/net/http
Timeouts, graceful shutdown, TLS configuration, connection limits, or proper lifecycle control, for these you should be constructing and owning a http.Server explicitly. This is where production engineering begins. Your server isn’t just a function call, it’s a component you need to manage.
The defaults are safe enough for Proof of Concepts and local development but not intentional enough for production. Understanding this distinction is what separates “I can write Go” from “I can build with Go.”
Handlers, Routing, and Structure
A major theme of the talk is structuring your handlers. Not just writing them, but actually structuring them. Good handler design lets you separate concerns, test your logic, and avoid creating API surfaces that are impossible to manage as your service grows.
func main() {
mux := http.NewServeMux()
// Go 1.22+ method and path parameter routing
mux.HandleFunc("GET /users/{id}", getUserHandler)
mux.HandleFunc("POST /users", createUserHandler)
mux.HandleFunc("DELETE /users/{id}", deleteUserHandler)
log.Println("Server starting on :8080")
log.Println("Try: curl <http://localhost:8080/users/123>")
log.Println("Try: curl -X POST <http://localhost:8080/users>")
log.Println("Try: curl -X DELETE <http://localhost:8080/users/123>")
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Fatal(err)
}source https://github.com/Soypete/talks/blob/main/go-meetup-nethttp/examples/02-routing-122/main.go
The core idea is simple: a handler is just a function with two arguments. How you layer your application around it determines whether your service is maintainable. When I build production APIs, I structure handlers with explicit dependencies, not hidden globals. That gives you clarity, testability, and control.
func getUserHandler(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "Getting user: %s\\n", id)
}This is exactly the kind of thing people assume requires a framework. It doesn’t. You can build whatever shape of abstraction you want using nothing but the standard library.
Middleware Without the Magic
Middleware is one of the most misunderstood parts of building APIs. The temptation is to find a framework that gives you a chain or a pipeline abstraction. But once again, the standard library already gives you everything you need without any magic.
A handler is a function. A middleware is a function that takes a handler and returns a new handler. That’s it. There’s no secret or hidden API.The entire pattern is already in front of you.
// Auth middleware
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token != "Bearer secret-token" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}In the talk, I walk through how to build your own middleware patterns using pure functions. It’s more explicit that way, and you can avoid the complexity that comes from adopting a framework’s lifecycle, especially in a service where your needs do not justify the cost of that abstraction.
Production-Grade Concerns: Timeouts, Shutdown, and Context
This is where most “Hello World” examples fall apart.
A production HTTP server needs:
well-scoped timeouts
controlled shutdown behavior
cancellation and propagation of context
defensive handling of slow clients
consistent logging and error reporting
These are crucial because they are the difference between a service that behaves predictably under load and one that hangs forever when a client disappears mid-request.
During the meetup, I walked through the http.Server timeout fields and how they relate to real request lifecycles. We spent time on graceful shutdown and how Go’s context system gives you a predictable path to unwind goroutines, flush buffers, and close connections.
This is the kind of engineering that people assume requires a framework or a library. It doesn’t. The standard library provides all the knobs you need. You just have to know where to look.
When You Shouldn’t Use net/http
There are times when you should not use the standard library directly.
If you need:
extremely complex routing rules
automatic OpenAPI generation
middleware chaining across dozens of concerns
build-time code generation
tightly integrated observability hooks
you may benefit from a larger framework or router. But the bar is higher than people think. If your service is relatively small, stable, or infrastructure-adjacent, net/http may not just be sufficient, it may even be the better choice.
As with most engineering decisions, the key is understanding your system’s needs rather than reaching for tools out of habit.
Code Examples
All of the code examples from the talk — including practical implementations of middleware, handler composition, timeouts, server configuration, and tests — live in the GitHub repository for this presentation:
GitHub: go-meetup-nethttp
https://github.com/Soypete/talks/tree/main/go-meetup-nethttp
If you want the annotated, commented, and extended versions of those examples, including the production patterns I use in my own services, those are available behind the SoyPetech paywall. Subscribers will get the full walkthrough and all code as downloadable files.
Closing Thoughts
Writing HTTP servers in Go has always been one of the language’s strongest features. But the real power shows up when you step beyond ListenAndServe and actually take control of your server. That’s when the clarity and intentional design of the standard library becomes obvious.
My hope is that this talk and now this written version helps more people realize that the standard library isn’t something you outgrow. It’s something you grow into. And once you do, you write cleaner, sharper, more reliable services with fewer dependencies and fewer surprises.
Stay Connected
Want to stay updated on what I’m working on? Here’s where you can find me:



