How Go’s Built‑In HTTP Server Handles Connections and Requests
This article walks through building a minimal Go HTTP server, explains how ListenAndServe manages connections, details the internal accept‑serve loop, shows request handling, routing rules, and customization hooks, providing clear code examples and diagrams to demystify Go’s built‑in web server.
Hello everyone, I am Xiao Lou, presenting the second article in the series "Go底层原理剖析", focusing on the HTTP module.
From a Demo
Creating a Go HTTP server is surprisingly simple; a functional server that responds to requests can be written in just 15 lines of code, including package declaration, imports, and an empty line.
package main
import (
"io"
"net/http"
)
func main() {
http.HandleFunc("/hello", hello)
http.ListenAndServe(":81", nil)
}
func hello(response http.ResponseWriter, request *http.Request) {
io.WriteString(response, "hello world")
}This level of simplicity rivals Python, and Go can compile the program into a standalone binary.
How Does the HTTP Server Handle Connections?
We start from the call http.ListenAndServe(":81", nil). The method both listens on the address and serves incoming connections, which mixes two responsibilities into one function.
The first argument Addr specifies the listening address and port; the second argument Handler is usually nil, meaning the default handler is used. Handlers are typically registered with http.HandleFunc, mapping a path to business logic.
http.HandleFunc("/hello", hello)Under the hood, Go wraps the system calls bind, listen, and accept inside ListenAndServe. The Listen part corresponds to the system call, while the Serve part contains the main loop that repeatedly calls Accept. When Accept returns a new connection, a new goroutine is started to handle it.
How Does the HTTP Server Process Requests?
Some Preparatory Work
Each connection is processed in its own goroutine:
go c.serve(connCtx)The connCtx carries the current server object:
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
...
connCtx := ctxThe server provides a hook srv.ConnContext that can modify the context for each new connection:
if cc := srv.ConnContext; cc != nil {
connCtx = cc(connCtx, rw)
if connCtx == nil {
panic("ConnContext returned nil")
}
}The hook’s definition is:
// ConnContext optionally specifies a function that modifies
// the context used for a new connection c. The provided ctx
// is derived from the base context and has a ServerContextKey value.
ConnContext func(ctx context.Context, c net.Conn) context.ContextTo customize it, we can create a server with an explicit ConnContext field:
func main() {
http.HandleFunc("/hello", hello)
server := http.Server{
Addr: ":81",
ConnContext: func(ctx context.Context, c net.Conn) context.Context {
return context.WithValue(ctx, "hello", "roshi")
},
}
server.ListenAndServe()
}The connection state hook c.setState and the ConnState callback work similarly, allowing actions on state changes.
c.setState(c.rwc, StateNew, runHooks) // before Serve can return // ConnState specifies an optional callback function that is
// called when a client connection changes state.
ConnState func(net.Conn, ConnState)Start Doing Real Work
To see what the serve method does after Accept, we simplify it:
func (c *conn) serve(ctx context.Context) {
...
for {
w, err := c.readRequest(ctx)
...
serverHandler{c.server}.ServeHTTP(w, w.req)
...
}
}The serve loop reads a request, then hands it to the registered handler. Each connection can handle multiple requests, so the loop runs continuously.
After reading a request, the server may start a background read for the next request, improving performance:
for {
w, err := c.readRequest(ctx)
...
if requestBodyRemains(req.Body) {
registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead)
} else {
w.conn.r.startBackgroundRead()
}
...
}How Requests Are Routed?
When a request is ready, the server calls:
serverHandler{c.server}.ServeHTTP(w, w.req)If the request URI is * or the method is OPTIONS, the built‑in globalOptionsHandler handles it automatically.
Routing follows three simple rules:
If a route with a host is registered, matching uses host+path; otherwise matching uses path only.
Exact matches have priority. If a registered pattern ends with /, prefix matching is also applied.
Examples:
Host‑based matching:
http.HandleFunc("/hello", hello)
http.HandleFunc("127.0.0.1/hello", hello2)Request curl 'http://127.0.0.1:81/hello' matches hello2, while curl 'http://localhost:81/hello' matches hello.
Prefix matching:
http.HandleFunc("/hello", hello)
http.HandleFunc("127.0.0.1/hello/", hello2)Request curl 'http://127.0.0.1:81/hello/roshi' matches hello2 because the pattern ends with /.
After routing, the registered handler is invoked; writing to the response sends data back to the client, completing the request.
Summary
Starting a Go HTTP server is extremely straightforward.
The server essentially runs a large loop, spawning a new goroutine for each incoming connection.
Each connection runs its own loop that reads requests, finds routes, and executes business logic.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Xiao Lou's Tech Notes
Backend technology sharing, architecture design, performance optimization, source code reading, troubleshooting, and pitfall practices
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
