Go HTTP Servers — Building Web Servers with net/http Package and Handlers
In this tutorial, you will learn about Go HTTP Servers. We cover key concepts, practical examples, and best practices to help you master this topic.
Go net/http package provides HTTP server capabilities with Handler interface, ServeMux for routing, and middleware for cross-cutting concerns.
What You'll Learn
- Basic HTTP server with net/http
- Handler and HandlerFunc
- Routing with ServeMux
- Middleware patterns
Why It Matters
Go is widely used for web services. Docker registry uses HTTP. Kubernetes API server is HTTP. DodaZIP serves file processing API over HTTP.
Real-World Use
REST APIs, Microservices, Webhook handlers, static file servers, reverse proxies.
flowchart LR
A["HTTP Server"] --> B["http.Handler"]
A --> C["ServeMux"]
A --> D["Middleware"]
A --> E["Server Config"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Basic HTTP Server
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
}
func main() {
http.HandleFunc("/", helloHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Handler Interface
type Greeter struct {
Name string
}
func (g *Greeter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", g.Name)
}
func main() {
http.Handle("/greet", &Greeter{Name: "World"})
log.Fatal(http.ListenAndServe(":8080", nil))
}
ServeMux Routing
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
mux.HandleFunc("GET /api/users/{id}", getUser)
mux.HandleFunc("PUT /api/users/{id}", updateUser)
mux.HandleFunc("DELETE /api/users/{id}", deleteUser)
log.Fatal(http.ListenAndServe(":8080", mux))
}
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "User ID: %s", id)
}
Middleware Pattern
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/data", dataHandler)
handler := loggingMiddleware(authMiddleware(mux))
log.Fatal(http.ListenAndServe(":8080", handler))
}
JSON API
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
var users = []User{{ID: 1, Name: "Alice"}}
func getUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
func createUser(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
users = append(users, user)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
Custom Server
func main() {
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx)
}
Common Mistakes
1. Not Setting Timeouts
// Bad: No timeouts can lead to resource exhaustion
http.ListenAndServe(":8080", nil)
2. Ignoring Request Cancellation
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Check ctx.Done() in long operations
}
3. Writing After Header
// Headers must be set before writing response body
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(data)
4. Not Closing Request Body
defer r.Body.Close() // Usually handled by net/http, but good practice
5. Panic in Handler
Use http.Handler wrapper with recover to prevent server crash.
Practice Questions
1. What is http.Handler? An interface with ServeHTTP(ResponseWriter, *Request). Any type implementing it serves HTTP.
2. What does ServeMux do? Routes requests to handlers based on method and path pattern. Supports path parameters with {id} syntax.
3. How do you read request body? Use json.NewDecoder(r.Body).Decode(&v) or io.ReadAll(r.Body).
4. How do you return JSON? Set Content-Type header and use json.NewEncoder to encode the response.
Challenge: Build a middleware chain that logs, authenticates, and rate-limits requests.
Solution
func chain(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
for _, m := range middlewares {
handler = m(handler)
}
return handler
}
// Usage:
handler := chain(myHandler, loggingMiddleware, authMiddleware, rateLimitMiddleware)
FAQ
{{< faq question="What is the difference between Handle and HandleFunc?" >}} Handle registers an http.Handler. HandleFunc registers a handler function. HandleFunc is syntactic sugar for HandlerFunc Adapter. {{< /faq >}}
{{< faq question="How do I serve static files?" >}}
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
{{< /faq >}}
{{< faq question="What is http.Server.Shutdown?" >}} Gracefully shuts down the server without interrupting active connections. Stops listening and waits for idle connections to close. {{< /faq >}}
{{< faq question="How do I handle CORS?" >}} Set CORS headers in middleware: Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers. {{< /faq >}}
{{< faq question="What frameworks extend net/http?" >}} Gin, Echo, Chi, and Fiber build on net/http. They add routing, middleware chaining, and context utilities. {{< /faq >}}
Try It Yourself
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, World!")
})
fmt.Println("Server starting on :8080")
http.ListenAndServe(":8080", mux)
}
Expected output — server listening on :8080, respond to GET /hello with "Hello, World!".
What's Next
Now that you understand HTTP servers, explore HTTP clients and external API calls.
| Topic | Description | Link |
|---|---|---|
| Go HTTP Client | Making HTTP requests | {{< ref "28-http-client" >}} |
| Go JSON | JSON encoding/decoding | {{< ref "26-json" >}} |
| Go Database | SQL database operations | {{< ref "29-database-sql" >}} |