GoNetworking

Go Web Server with net/http: Complete Guide

TT
TopicTrick Team
Go Web Server with net/http: Complete Guide

Go Web Server with net/http: Complete Guide

Go was designed at Google to solve modern networking challenges. As a result, its standard library for HTTP is world-class. You can build a production-grade, highly-performant web server without ever reaching for an external framework like Gin or Echo.

In this module, we will explore the net/http package and learn how to map URLs to logic using the ServeMux.

Building a Go Web Server with net/http

Go's net/http package provides a complete HTTP server and client in the standard library. To start a server, register handler functions against URL patterns on a ServeMux, then call http.ListenAndServe. Each incoming request is handled in its own goroutine, giving you concurrency out of the box with no thread pool configuration required.

Production-Ready by Design

    The "Hello World" of Web Servers

    To get started, you need two things: a Handler function and a call to ListenAndServe.

    go

    Understanding Request & Response

    Wait, let's look closer at the handler's arguments. They are the keys to interacting with your users.

    HTTP Mechanics

    ResponseWriterw.WriteHeader(200)

    The interface used to construct the HTTP response. You use it to set status codes, headers, and write the body.

    http.Requestr.Method, r.Header

    A pointer to a struct containing everything about the incoming request: parameters, bodies, cookies, and more.

    Muxerhttp.NewServeMux()

    The HTTP request multiplexer. It matches the URL of each incoming request against a list of registered patterns.

    Handling Different HTTP Methods

    Unlike some frameworks, net/http handlers receive ALL requests to their path. You must manually check the method if you want to differentiate between a GET and a POST.

    go

    Serving Static Files

    A common task for a web server is serving assets like CSS, JavaScript, or images. Go makes this trivial with http.FileServer.

    go
    Task / FeatureStandard net/httpThird-Party Frameworks
    DependencyZero (Built-in)External (Increases binary size)
    SpeedRaw performanceSlight overhead for routing features
    SimplicityExplicit and minimalAbstractions can hide detail

    Custom ServeMux and Route Parameters

    Go 1.22 significantly improved the built-in ServeMux with method-based routing and path parameters, making third-party routers optional for many use cases:

    go

    See the official ServeMux documentation for the complete pattern matching specification.

    Graceful Shutdown

    A production server must finish in-flight requests when it receives a shutdown signal (e.g., SIGTERM during a Kubernetes rolling update). Go's http.Server exposes a Shutdown method for exactly this purpose.

    go

    Setting Timeouts

    A server without timeouts is vulnerable to slowloris attacks and resource exhaustion. Always configure read, write, and idle timeouts on your http.Server struct.

    go

    Timeout configuration connects directly with the security practices in the Go security best practices guide — a missing write timeout is one of the top attack vectors against Go HTTP servers.

    Reading Request Bodies

    For API endpoints that accept POST data, you must explicitly decode the request body and enforce size limits to prevent memory exhaustion:

    go

    This body-reading pattern is used throughout the Go REST API project guide alongside a full layered architecture.

    Applying Middleware to the Server

    Middleware wraps the mux to add cross-cutting concerns like logging and recovery. The full implementation is covered in Go middleware patterns, but here is how it plugs into your server setup:

    go

    The Go HTTP tracing blog post explains advanced request lifecycle hooks available in net/http for observability tooling.

    Common Mistakes and How to Avoid Them

    Go's net/http package is straightforward, but several non-obvious mistakes can cause production issues.

    1. Using the default http.DefaultServeMux. Calling http.HandleFunc registers routes on a global DefaultServeMux. Third-party packages that import net/http may also register routes on this mux — including debug endpoints. Always create an explicit mux := http.NewServeMux() and pass it to http.ListenAndServe.

    2. Not setting server-level timeouts. A server created with http.ListenAndServe(":8080", mux) has no timeouts. Malicious or slow clients can hold connections open indefinitely, exhausting goroutine resources. Always construct an http.Server struct with ReadTimeout, WriteTimeout, and IdleTimeout set explicitly. The Go net/http package documentation lists all available timeout fields.

    3. Serving large files without http.ServeContent. Reading an entire file into memory before writing it to the response causes large memory spikes under concurrent load. Use http.ServeFile or http.ServeContent instead — they use io.Copy internally to stream the file in chunks and support Range requests for partial content delivery.

    4. Forgetting defer resp.Body.Close() on outbound HTTP clients. When making outbound requests with http.Client.Get, the response body must always be closed. Even if you read the body in full, failing to close it prevents the connection from being returned to the keep-alive pool, causing file descriptor exhaustion over time.

    5. Blocking the main goroutine in a handler. Handlers run in dedicated goroutines, so one slow handler does not directly block others. However, spawning long-running goroutines inside a handler without coordinating their lifecycle leads to goroutine leaks if the request context is cancelled. Use r.Context() to detect cancellation and stop background work promptly. The Go concurrency patterns blog post illustrates lifecycle-aware goroutine patterns.

    FAQ

    Q: When should I use http.ServeMux versus a third-party router like Chi or Gorilla Mux?

    The Go 1.22+ ServeMux supports method-based routing and path parameters, which covers the needs of most REST APIs. Choose a third-party router when you need middleware chains with per-group application, wildcard patterns beyond what ServeMux supports, or deep integration with OpenAPI spec generation. Chi is the most idiomatic choice because it follows the standard http.Handler interface.

    Q: How do I serve a React or Next.js frontend from a Go server?

    Use http.FileServer(http.Dir("./dist")) to serve the built frontend assets from a directory. Wrap it with http.StripPrefix("/", ...) and register it as a catch-all route. For single-page applications that use client-side routing, you must serve index.html for any URL that does not match a real file — implement a custom handler that falls back to serving index.html on 404.

    Q: How do I add HTTPS to a net/http server?

    Replace http.ListenAndServe with http.ListenAndServeTLS("cert.pem", "key.pem", mux). For automatic certificate management via Let's Encrypt, use golang.org/x/crypto/acme/autocert. In production Kubernetes deployments, TLS is typically terminated at the ingress controller, so the Go server itself runs on plain HTTP inside the cluster.

    Next Steps

    Building a server is only half the battle. To communicate with modern frontends or other microservices, you need to handle data efficiently. In our next tutorial, we will explore Working with JSON in Go, where you'll learn how to transform Go structs into API-friendly JSON and back again.

    Common Go HTTP Server Mistakes

    1. Using the default http.ServeMux for production The default mux is a global — registering handlers on it affects all tests and can cause conflicts in larger codebases. Create an explicit mux := http.NewServeMux() and pass it to your server.

    2. Not setting server timeouts http.ListenAndServe(":8080", mux) has no timeouts — a slow client can hold a connection open indefinitely. Always configure ReadTimeout, WriteTimeout, and IdleTimeout:

    go

    3. Not reading and discarding the request body before close If you want HTTP/1.1 keep-alive, the full request body must be consumed. If your handler returns early without reading the body, add io.Copy(io.Discard, r.Body) before returning to allow connection reuse.

    4. Using fmt.Fprintf(w, ...) and ignoring the error ResponseWriter.Write can fail (e.g. client disconnect). In most handlers the error can be ignored, but for debugging always wrap writes in error checks or use a response writer that tracks the write status.

    5. Not using HTTPS in production Go's net/http makes TLS trivial. Use http.ListenAndServeTLS or — even better — put a reverse proxy like Caddy or nginx in front and let it handle TLS termination. See the Go net/http package documentation for TLS configuration options.

    Frequently Asked Questions

    What is the difference between http.Handle and http.HandleFunc? http.Handle(pattern, handler) registers an http.Handler interface value. http.HandleFunc(pattern, func) is a convenience wrapper that converts a plain function with the signature func(ResponseWriter, *Request) into an http.Handler. Use HandleFunc for simple handlers and Handle when you need to pass a struct with methods.

    How do I serve static files in Go? Use http.FileServer(http.Dir("./static")). For embedding static files into the binary, use Go's embed package (available since Go 1.16): //go:embed static embeds the directory at compile time. The Go embed package documentation shows the full usage.

    How do I handle CORS in a Go HTTP server? Implement a middleware that sets Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers response headers, and responds to OPTIONS preflight requests with 204. Libraries like rs/cors handle all the edge cases if you prefer a ready-made solution.