Skip to content

Go HTTP Servers — Building Web Servers with net/http Package and Handlers

DodaTech Updated 2026-06-28 4 min read

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" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro

Home Browse Go