How do you build a basic HTTP server using only the standard library?
Quick Answer
net/http provides everything needed for a working server: register handlers with http.HandleFunc(pattern, handlerFunc) or by building an http.ServeMux, then start listening with http.ListenAndServe(addr, mux). A handler is any function matching func(w http.ResponseWriter, r *http.Request), or any type implementing the http.Handler interface's single ServeHTTP method — HandleFunc is just a convenience adapter around that interface. Since Go 1.22, http.ServeMux itself supports method-based routing and path parameters natively (mux.HandleFunc("GET /users/{id}", handler)), which closed much of the gap that used to push people toward third-party routers for anything beyond the simplest routing needs.
Detailed Answer
A production-capable HTTP server in Go needs zero external dependencies to get started.
The simplest possible server
package main
import (
"fmt"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, world!")
}
func main() {
http.HandleFunc("/hello", helloHandler)
http.ListenAndServe(":8080", nil)
}
nil as the second argument to ListenAndServe means "use the default ServeMux," the package-level router http.HandleFunc registers against implicitly.
Method-based routing and path parameters (Go 1.22+)
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "user id: %s", id)
})
mux.HandleFunc("POST /users", createUserHandler)
http.ListenAndServe(":8080", mux)
Before Go 1.22, the standard ServeMux couldn't route by HTTP method or extract path parameters, which is why third-party routers like gorilla/mux or chi were so popular. Go 1.22 added both directly to the standard library's mux.
The http.Handler interface underneath it all
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
http.HandleFunc is just a small adapter (http.HandlerFunc) that lets a plain function satisfy this interface. Anything implementing ServeHTTP directly, like a custom struct with configuration baked in, works equally well as a handler.
Setting up a real server, not just ListenAndServe
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
srv.ListenAndServe()
Using an explicit http.Server struct, rather than the package-level http.ListenAndServe shortcut, is the production-appropriate pattern, since it lets you set timeouts and supports graceful shutdown, covered in a later question.