HTTP Programming

Lesson 22: HTTP Programming

🎯 Life Analogy

Imagine you run a restaurant:

Go's net/http package is your "restaurant management system," helping you quickly build a high-performance, stable web service.


📚 Core Concepts

Concept Description
http.Get/Post Send HTTP requests to retrieve remote resources
http.ListenAndServe Start an HTTP server and listen on a port
Handler Interface The core interface that defines request handling standards
HandlerFunc An adapter that converts a regular function into a Handler
ServeMux HTTP request multiplexer (router)
Middleware A pattern for inserting common logic before and after request handling

📝 Basic Syntax and Usage

1. Sending HTTP Requests

GO
package main

import (
	"fmt"
	"io"
	"net/http"
)

func main() {
	// Send GET request
	resp, err := http.Get("https://httpbin.org/get")
	if err != nil {
		fmt.Println("Request failed:", err)
		return
	}
	defer resp.Body.Close() // 💡 Must close the response body to avoid resource leaks

	// Read response content
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("Read failed:", err)
		return
	}

	fmt.Println("Status code:", resp.StatusCode)
	fmt.Println("Response:", string(body))
}
💡 Tip: resp.Body must be closed after use with Close(), otherwise it will cause connection leaks. Using defer is the best practice.

2. Sending POST Requests

GO
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

func main() {
	// Construct JSON data
	data := map[string]string{
		"username": "gopher",
		"email":    "gopher@example.com",
	}
	jsonData, _ := json.Marshal(data)

	// Send POST request
	// 💡 The third parameter is the request body, requires io.Reader type
	resp, err := http.Post(
		"https://httpbin.org/post",
		"application/json",
		bytes.NewBuffer(jsonData),
	)
	if err != nil {
		fmt.Println("Request failed:", err)
		return
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	fmt.Println("Status code:", resp.StatusCode)
	fmt.Println("Response:", string(body))
}
💡 Tip: The Content-Type parameter of http.Post is very important — the server relies on it to parse the request body format.

3. Starting an HTTP Server

GO
package main

import (
	"fmt"
	"net/http"
)

func main() {
	// Register route handlers
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Welcome to the homepage!")
	})

	http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello, Gopher!")
	})

	// Start server, listen on port 8080
	// 💡 ListenAndServe blocks until the server shuts down
	fmt.Println("Server started at http://localhost:8080")
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		fmt.Println("Server failed to start:", err)
	}
}
💡 Tip: Passing nil as the second parameter means using the default DefaultServeMux. In production, it's recommended to create a custom router.

4. Handler Interface

GO
// Handler interface definition: any type that implements the ServeHTTP method is a Handler
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}
GO
package main

import (
	"fmt"
	"net/http"
)

// Custom Handler type
type GreetingHandler struct {
	Message string
}

// Implement the ServeHTTP method of the Handler interface
func (g GreetingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, g.Message)
}

func main() {
	// Use custom Handler
	handler := GreetingHandler{Message: "Hello, this is a custom Handler!"}
	http.Handle("/greet", handler) // 💡 Note: use Handle, not HandleFunc

	http.ListenAndServe(":8080", nil)
}
💡 Tip: Handle accepts a Handler interface, HandleFunc accepts a function. Both are functionally equivalent, just different forms.

5. HandlerFunc Adapter

GO
package main

import (
	"fmt"
	"net/http"
)

func myHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Handler adapted via HandlerFunc")
}

func main() {
	// HandlerFunc converts a regular function to a Handler
	// 💡 Essentially a type conversion: type HandlerFunc func(ResponseWriter, *Request)
	http.Handle("/adapted", http.HandlerFunc(myHandler))

	// Equivalent form (more commonly used)
	http.HandleFunc("/simple", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Using HandleFunc directly is more concise")
	})

	http.ListenAndServe(":8080", nil)
}
💡 Tip: HandlerFunc is a type adapter that allows regular functions to satisfy the Handler interface.


🧪 Practical Examples

Example: Simple Static File Server (Difficulty ⭐)

GO
package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	// Register static file service using the default router
	// StripPrefix removes the "/static/" prefix from the URL
	// FileServer provides access to files in the directory
	fs := http.FileServer(http.Dir("./public"))
	http.Handle("/static/", http.StripPrefix("/static/", fs))

	// Homepage handler
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/" {
			http.NotFound(w, r) // 💡 404 handling
			return
		}
		fmt.Fprintf(w, "Welcome to my website!")
	})

	fmt.Println("Server running at http://localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}
▶ Try it Yourself

Running:

BASH
# Create test directory and files
mkdir -p public
echo "<h1>Hello</h1>" > public/index.html

# Run the server
go run main.go

# Test access (another terminal)
curl http://localhost:8080/
curl http://localhost:8080/static/index.html

Example: Custom Routing and Middleware (Difficulty ⭐⭐)

GO
package main

import (
	"fmt"
	"log"
	"net/http"
	"time"
)

// Logging middleware: records processing time for each request
func LoggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		log.Printf("Start %s %s", r.Method, r.URL.Path)

		next.ServeHTTP(w, r) // 💡 Call the next handler

		log.Printf("Done %s %s took %v", r.Method, r.URL.Path, time.Since(start))
	})
}

// Auth middleware: checks Token in request headers
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 access", http.StatusUnauthorized)
			return
		}
		log.Printf("User Token: %s", token)
		next.ServeHTTP(w, r)
	})
}

func main() {
	mux := http.NewServeMux() // 💡 Create custom router

	// Public routes
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Welcome to the API service!")
	})

	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"status": "ok"}`)
	})

	// Routes requiring authentication
	protected := http.NewServeMux()
	protected.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "This is your profile page")
	})
	protected.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "This is the settings page")
	})

	// 💡 Middleware chain: logging first, then auth, then handle
	mux.Handle("/api/", AuthMiddleware(protected))

	// Apply logging middleware to all routes
	finalHandler := LoggingMiddleware(mux)

	fmt.Println("Server running at http://localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", finalHandler))
}
▶ Try it Yourself

Running and Testing:

BASH
# Run
go run main.go

# Test public routes
curl http://localhost:8080/
curl http://localhost:8080/health

# Test authenticated routes (no Token)
curl http://localhost:8080/api/profile
# Output: Unauthorized access

# Test authenticated routes (with Token)
curl -H "Authorization: Bearer mytoken123" http://localhost:8080/api/profile
# Output: This is your profile page

Example: Complete RESTful API Service (Difficulty ⭐⭐⭐)

GO
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strconv"
	"strings"
	"sync"
	"time"
)

// User model
type User struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
}

// APIResponse unified response format
type APIResponse struct {
	Code    int         `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data,omitempty"`
}

// UserStore user storage (using map to simulate database)
type UserStore struct {
	mu     sync.RWMutex
	users  map[int]*User
	nextID int
}

func NewUserStore() *UserStore {
	return &UserStore{
		users:  make(map[int]*User),
		nextID: 1,
	}
}

func (s *UserStore) Create(name, email string) *User {
	s.mu.Lock()
	defer s.mu.Unlock()

	user := &User{
		ID:    s.nextID,
		Name:  name,
		Email: email,
	}
	s.users[s.nextID] = user
	s.nextID++
	return user
}

func (s *UserStore) Get(id int) *User {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.users[id]
}

func (s *UserStore) List() []*User {
	s.mu.RLock()
	defer s.mu.RUnlock()

	users := make([]*User, 0, len(s.users))
	for _, u := range s.users {
		users = append(users, u)
	}
	return users
}

func (s *UserStore) Update(id int, name, email string) *User {
	s.mu.Lock()
	defer s.mu.Unlock()

	user, ok := s.users[id]
	if !ok {
		return nil
	}
	if name != "" {
		user.Name = name
	}
	if email != "" {
		user.Email = email
	}
	return user
}

func (s *UserStore) Delete(id int) bool {
	s.mu.Lock()
	defer s.mu.Unlock()

	if _, ok := s.users[id]; !ok {
		return false
	}
	delete(s.users, id)
	return true
}

// UserHandler user API handler
type UserHandler struct {
	store *UserStore
}

// writeJSON writes a JSON response
func (h *UserHandler) writeJSON(w http.ResponseWriter, code int, resp APIResponse) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code)
	json.NewEncoder(w).Encode(resp)
}

// List get user list GET /users
func (h *UserHandler) List(w http.ResponseWriter, r *http.Request) {
	users := h.store.List()
	h.writeJSON(w, http.StatusOK, APIResponse{
		Code:    0,
		Message: "success",
		Data:    users,
	})
}

// Create create user POST /users
func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Name  string `json:"name"`
		Email string `json:"email"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		h.writeJSON(w, http.StatusBadRequest, APIResponse{
			Code:    1,
			Message: "Invalid request data",
		})
		return
	}

	if req.Name == "" || req.Email == "" {
		h.writeJSON(w, http.StatusBadRequest, APIResponse{
			Code:    2,
			Message: "Name and email cannot be empty",
		})
		return
	}

	user := h.store.Create(req.Name, req.Email)
	h.writeJSON(w, http.StatusCreated, APIResponse{
		Code:    0,
		Message: "Created successfully",
		Data:    user,
	})
}

// Get get single user GET /users/{id}
func (h *UserHandler) Get(w http.ResponseWriter, r *http.Request) {
	idStr := strings.TrimPrefix(r.URL.Path, "/users/")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		h.writeJSON(w, http.StatusBadRequest, APIResponse{
			Code:    1,
			Message: "Invalid user ID",
		})
		return
	}

	user := h.store.Get(id)
	if user == nil {
		h.writeJSON(w, http.StatusNotFound, APIResponse{
			Code:    3,
			Message: "User not found",
		})
		return
	}

	h.writeJSON(w, http.StatusOK, APIResponse{
		Code:    0,
		Message: "success",
		Data:    user,
	})
}

// Update update user PUT /users/{id}
func (h *UserHandler) Update(w http.ResponseWriter, r *http.Request) {
	idStr := strings.TrimPrefix(r.URL.Path, "/users/")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		h.writeJSON(w, http.StatusBadRequest, APIResponse{
			Code:    1,
			Message: "Invalid user ID",
		})
		return
	}

	var req struct {
		Name  string `json:"name"`
		Email string `json:"email"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		h.writeJSON(w, http.StatusBadRequest, APIResponse{
			Code:    1,
			Message: "Invalid request data",
		})
		return
	}

	user := h.store.Update(id, req.Name, req.Email)
	if user == nil {
		h.writeJSON(w, http.StatusNotFound, APIResponse{
			Code:    3,
			Message: "User not found",
		})
		return
	}

	h.writeJSON(w, http.StatusOK, APIResponse{
		Code:    0,
		Message: "Updated successfully",
		Data:    user,
	})
}

// Delete delete user DELETE /users/{id}
func (h *UserHandler) Delete(w http.ResponseWriter, r *http.Request) {
	idStr := strings.TrimPrefix(r.URL.Path, "/users/")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		h.writeJSON(w, http.StatusBadRequest, APIResponse{
			Code:    1,
			Message: "Invalid user ID",
		})
		return
	}

	if !h.store.Delete(id) {
		h.writeJSON(w, http.StatusNotFound, APIResponse{
			Code:    3,
			Message: "User not found",
		})
		return
	}

	h.writeJSON(w, http.StatusOK, APIResponse{
		Code:    0,
		Message: "Deleted successfully",
	})
}

// CORS middleware
func CORSMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

		if r.Method == "OPTIONS" {
			w.WriteHeader(http.StatusOK)
			return
		}

		next.ServeHTTP(w, r)
	})
}

// Recovery middleware: catch panics
func RecoveryMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				log.Printf("Caught panic: %v", err)
				http.Error(w, "Internal server error", http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

func main() {
	store := NewUserStore()
	handler := &UserHandler{store: store}

	// Create router
	mux := http.NewServeMux()

	// 💡 Register routes: dispatch to different handlers based on HTTP method
	mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
		switch r.Method {
		case http.MethodGet:
			handler.List(w, r)
		case http.MethodPost:
			handler.Create(w, r)
		default:
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		}
	})

	// 💡 Handle routes with path parameters /users/{id}
	mux.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
		switch r.Method {
		case http.MethodGet:
			handler.Get(w, r)
		case http.MethodPut:
			handler.Update(w, r)
		case http.MethodDelete:
			handler.Delete(w, r)
		default:
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		}
	})

	// Apply middleware chain
	finalHandler := RecoveryMiddleware(CORSMiddleware(mux))

	// Create server (configurable timeouts)
	server := &http.Server{
		Addr:         ":8080",
		Handler:      finalHandler,
		ReadTimeout:  10 * time.Second,  // 💡 Read timeout
		WriteTimeout: 10 * time.Second,  // 💡 Write timeout
	}

	// Insert test data
	store.Create("Alice", "alice@example.com")
	store.Create("Bob", "bob@example.com")

	fmt.Println("RESTful API server running at http://localhost:8080")
	fmt.Println("Available endpoints:")
	fmt.Println("  GET    /users      - Get user list")
	fmt.Println("  POST   /users      - Create user")
	fmt.Println("  GET    /users/{id} - Get single user")
	fmt.Println("  PUT    /users/{id} - Update user")
	fmt.Println("  DELETE /users/{id} - Delete user")

	log.Fatal(server.ListenAndServe())
}
▶ Try it Yourself

Running and Full Test:

BASH
# Run server
go run main.go

# Get user list
curl http://localhost:8080/users

# Create user
curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Charlie", "email": "charlie@example.com"}'

# Get single user
curl http://localhost:8080/users/1

# Update user
curl -X PUT http://localhost:8080/users/1 \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice Smith"}'

# Delete user
curl -X DELETE http://localhost:8080/users/3

Expected Output Example:

TEXT
# GET /users
{"code":0,"message":"success","data":[{"id":1,"name":"Alice","email":"alice@example.com"},{"id":2,"name":"Bob","email":"bob@example.com"}]}

# POST /users
{"code":0,"message":"Created successfully","data":{"id":3,"name":"Charlie","email":"charlie@example.com"}}

# GET /users/1
{"code":0,"message":"success","data":{"id":1,"name":"Alice","email":"alice@example.com"}}

🎬 Scenario Walkthrough

Scenario 1: Building a Weather Query Service

GO
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
)

// WeatherResponse simulated weather API response
type WeatherResponse struct {
	City        string  `json:"city"`
	Temperature float64 `json:"temperature"`
	Condition   string  `json:"condition"`
	Humidity    int     `json:"humidity"`
}

// WeatherService weather service handler
type WeatherService struct {
	// Simulated data
	data map[string]WeatherResponse
}

func NewWeatherService() *WeatherService {
	return &WeatherService{
		data: map[string]WeatherResponse{
			"beijing":  {City: "Beijing", Temperature: 28.5, Condition: "Sunny", Humidity: 45},
			"shanghai": {City: "Shanghai", Temperature: 30.2, Condition: "Cloudy", Humidity: 65},
			"guangzhou": {City: "Guangzhou", Temperature: 33.1, Condition: "Thunderstorm", Humidity: 80},
		},
	}
}

// GetWeather query weather GET /weather?city=beijing
func (s *WeatherService) GetWeather(w http.ResponseWriter, r *http.Request) {
	// 💡 Get city name from URL query parameters
	city := r.URL.Query().Get("city")
	if city == "" {
		http.Error(w, `{"error": "Please provide the city parameter"}`, http.StatusBadRequest)
		return
	}

	// URL decode (supports Chinese city names)
	city, _ = url.QueryUnescape(city)

	// Simulate finding weather data
	weather, ok := s.data[city]
	if !ok {
		http.Error(w, fmt.Sprintf(`{"error": "City not found: %s"}`, city), http.StatusNotFound)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(weather)
}

// GetForecast forecast list GET /forecast
func (s *WeatherService) GetForecast(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")

	// Return all city weather
	forecasts := make([]WeatherResponse, 0, len(s.data))
	for _, v := range s.data {
		forecasts = append(forecasts, v)
	}

	json.NewEncoder(w).Encode(map[string]interface{}{
		"count":     len(forecasts),
		"forecasts": forecasts,
	})
}

func main() {
	service := NewWeatherService()

	mux := http.NewServeMux()
	mux.HandleFunc("/weather", service.GetWeather)
	mux.HandleFunc("/forecast", service.GetForecast)

	fmt.Println("Weather service running at http://localhost:8080")
	fmt.Println("Usage:")
	fmt.Println("  GET /weather?city=beijing")
	fmt.Println("  GET /forecast")

	http.ListenAndServe(":8080", mux)
}
BASH
# Test
curl "http://localhost:8080/weather?city=beijing"
# {"city":"Beijing","temperature":28.5,"condition":"Sunny","humidity":45}

curl http://localhost:8080/forecast
# {"count":3,"forecasts":[...]}

Scenario 2: Implementing Rate Limiting Middleware

GO
package main

import (
	"fmt"
	"net/http"
	"sync"
	"time"
)

// RateLimiter token bucket rate limiter
type RateLimiter struct {
	mu       sync.Mutex
	tokens   map[string]int
	limit    int
	interval time.Duration
	lastTick map[string]time.Time
}

func NewRateLimiter(limit int, interval time.Duration) *RateLimiter {
	return &RateLimiter{
		tokens:   make(map[string]int),
		limit:    limit,
		interval: interval,
		lastTick: make(map[string]time.Time),
	}
}

// Allow checks if a request is allowed through
func (rl *RateLimiter) Allow(key string) bool {
	rl.mu.Lock()
	defer rl.mu.Unlock()

	now := time.Now()
	last, exists := rl.lastTick[key]

	if !exists || now.Sub(last) >= rl.interval {
		// 💡 Reset tokens
		rl.tokens[key] = rl.limit
		rl.lastTick[key] = now
	}

	if rl.tokens[key] <= 0 {
		return false
	}

	rl.tokens[key]--
	return true
}

// RateLimitMiddleware rate limiting middleware
func RateLimitMiddleware(limiter *RateLimiter) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			// Use client IP as rate limiting key
			clientIP := r.RemoteAddr

			if !limiter.Allow(clientIP) {
				w.Header().Set("Content-Type", "application/json")
				w.WriteHeader(http.StatusTooManyRequests) // 429
				fmt.Fprintf(w, `{"error": "Too many requests, please try again later"}`)
				return
			}

			next.ServeHTTP(w, r)
		})
	}
}

func main() {
	// Create rate limiter: max 10 requests per minute per IP
	limiter := NewRateLimiter(10, time.Minute)

	mux := http.NewServeMux()

	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Request successful! Time: %s", time.Now().Format("15:04:05"))
	})

	mux.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"data": "This is API data", "time": "%s"}`, time.Now().Format("15:04:05"))
	})

	// Apply rate limiting middleware
	handler := RateLimitMiddleware(limiter)(mux)

	fmt.Println("Rate limiting server running at http://localhost:8080")
	fmt.Println("Rate limit rule: max 10 requests per minute")
	http.ListenAndServe(":8080", handler)
}
BASH
# Test rate limiting (send multiple requests quickly)
for i in {1..15}; do
  curl -s http://localhost:8080/
  echo ""
done
# First 10 requests return normally, subsequent ones return 429 error

❓ FAQ

Q1: What's the difference between http.HandleFunc and http.Handle?

GO
// HandleFunc accepts a function
http.HandleFunc("/path", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello")
})

// Handle accepts a Handler interface
type MyHandler struct{}
func (h MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello")
}
http.Handle("/path", MyHandler{})

Difference:


Q2: How to get request parameters in a handler function?

GO
func handler(w http.ResponseWriter, r *http.Request) {
	// 💡 Query parameters (?key=value in URL)
	name := r.URL.Query().Get("name")

	// 💡 Form data (POST form-urlencoded)
	r.ParseForm()
	email := r.Form.Get("email")

	// 💡 Path parameters (need to parse manually or use third-party router)
	// e.g., 123 in /users/123
	path := r.URL.Path // "/users/123"
	// Manual parsing: strings.TrimPrefix(path, "/users/")

	// 💡 Request headers
	token := r.Header.Get("Authorization")

	// 💡 JSON request body
	var data map[string]string
	json.NewDecoder(r.Body).Decode(&data)

	fmt.Fprintf(w, "name=%s, email=%s, token=%s", name, email, token)
}

Q3: How to implement graceful server shutdown?

GO
package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	server := &http.Server{
		Addr: ":8080",
		Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			time.Sleep(2 * time.Second) // Simulate time-consuming operation
			fmt.Fprintf(w, "Processing complete")
		}),
	}

	// Start server in a goroutine
	go func() {
		fmt.Println("Server started at :8080")
		if err := server.ListenAndServe(); err != http.ErrServerClosed {
			log.Fatalf("Server error: %v", err)
		}
	}()

	// 💡 Listen for system signals
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit // Block and wait for signal

	fmt.Println("\nGracefully shutting down server...")

	// 💡 Give existing requests 5 seconds to complete
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	if err := server.Shutdown(ctx); err != nil {
		log.Fatalf("Shutdown failed: %v", err)
	}

	fmt.Println("Server shut down safely")
}
BASH
# After running, press Ctrl+C to trigger graceful shutdown
go run main.go
# Output: Server started at :8080
# Press Ctrl+C
# Output: Gracefully shutting down server...
# Output: Server shut down safely

GO
// ❌ Using default DefaultServeMux (not recommended)
http.HandleFunc("/api/users", usersHandler)
http.ListenAndServe(":8080", nil) // nil means using DefaultServeMux

// ✅ Using custom Mux (recommended)
mux := http.NewServeMux()
mux.HandleFunc("/api/users", usersHandler)
http.ListenAndServe(":8080", mux)

Reasons:

  1. DefaultServeMux is global — any package can register routes, which may cause conflicts
  2. Custom Mux has controllable scope, avoiding unexpected route overrides
  3. Easier to reuse and test across different server instances

📖 Summary

This lesson covered the core content of Go HTTP programming:

Knowledge Point Key Takeaways
HTTP Client http.Get/Post to send requests, resp.Body must be closed
HTTP Server http.ListenAndServe to start service, route registration
Handler Interface ServeHTTP(ResponseWriter, *Request) method
HandlerFunc Function adapter, converts function to Handler
ServeMux Router, supports prefix matching
Middleware Wraps Handler, inserts common logic before/after requests
Graceful Shutdown Use server.Shutdown and signal handling

Core Pattern:

TEXT
Client Request → Middleware Chain → Route Matching → Handler Processing → Response Return

📝 Exercises

Exercise 1: Implement File Upload Service

Requirements:

Reference Solution
GO
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
)

func uploadHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Only POST method is supported", http.StatusMethodNotAllowed)
		return
	}

	// 💡 Limit upload size to 10MB
	r.ParseMultipartForm(10 << 20)

	file, header, err := r.FormFile("file")
	if err != nil {
		http.Error(w, "Failed to get file", http.StatusBadRequest)
		return
	}
	defer file.Close()

	// Check file type
	ext := strings.ToLower(filepath.Ext(header.Filename))
	allowed := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".gif": true}
	if !allowed[ext] {
		http.Error(w, "Only image files are allowed", http.StatusBadRequest)
		return
	}

	// Save file
	os.MkdirAll("./uploads", 0755)
	dst, err := os.Create(filepath.Join("./uploads", header.Filename))
	if err != nil {
		http.Error(w, "Failed to save file", http.StatusInternalServerError)
		return
	}
	defer dst.Close()

	io.Copy(dst, file)

	fmt.Fprintf(w, `{"url": "/files/%s", "size": %d}`, header.Filename, header.Size)
}

func main() {
	http.HandleFunc("/upload", uploadHandler)
	http.Handle("/files/", http.StripPrefix("/files/", http.FileServer(http.Dir("./uploads"))))

	fmt.Println("File upload service running at http://localhost:8080")
	http.ListenAndServe(":8080", nil)
}

Exercise 2: Implement JWT Authentication Middleware

Requirements:

Reference Solution
GO
package main

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"time"
)

// Simplified JWT structure
type SimpleToken struct {
	Username string    `json:"username"`
	ExpireAt time.Time `json:"expire"`
}

// Generate simple token
func generateToken(username string) (string, error) {
	token := SimpleToken{
		Username: username,
		ExpireAt: time.Now().Add(1 * time.Hour),
	}
	data, err := json.Marshal(token)
	if err != nil {
		return "", err
	}
	return base64.StdEncoding.EncodeToString(data), nil
}

// Validate token
func validateToken(tokenStr string) (*SimpleToken, error) {
	data, err := base64.StdEncoding.DecodeString(tokenStr)
	if err != nil {
		return nil, fmt.Errorf("invalid token")
	}

	var token SimpleToken
	if err := json.Unmarshal(data, &token); err != nil {
		return nil, fmt.Errorf("token parse failed")
	}

	if time.Now().After(token.ExpireAt) {
		return nil, fmt.Errorf("token expired")
	}

	return &token, nil
}

// Auth middleware
func AuthMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		auth := r.Header.Get("Authorization")
		if auth == "" {
			http.Error(w, `{"error": "Please provide Authorization header"}`, http.StatusUnauthorized)
			return
		}

		// Parse "Bearer <token>"
		parts := strings.SplitN(auth, " ", 2)
		if len(parts) != 2 || parts[0] != "Bearer" {
			http.Error(w, `{"error": "Invalid Authorization format"}`, http.StatusUnauthorized)
			return
		}

		token, err := validateToken(parts[1])
		if err != nil {
			http.Error(w, fmt.Sprintf(`{"error": "%s"}`, err), http.StatusUnauthorized)
			return
		}

		// Store user info in request header (simplified)
		r.Header.Set("X-Username", token.Username)
		next.ServeHTTP(w, r)
	})
}

func main() {
	mux := http.NewServeMux()

	// Login endpoint
	mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			http.Error(w, "Only POST is supported", http.StatusMethodNotAllowed)
			return
		}

		var req struct {
			Username string `json:"username"`
			Password string `json:"password"`
		}
		json.NewDecoder(r.Body).Decode(&req)

		// Simple validation (should check database in production)
		if req.Username != "admin" || req.Password != "123456" {
			http.Error(w, `{"error": "Invalid username or password"}`, http.StatusUnauthorized)
			return
		}

		token, _ := generateToken(req.Username)
		fmt.Fprintf(w, `{"token": "%s"}`, token)
	})

	// Protected endpoint
	protected := http.NewServeMux()
	protected.HandleFunc("/protected", func(w http.ResponseWriter, r *http.Request) {
		username := r.Header.Get("X-Username")
		fmt.Fprintf(w, `{"message": "Welcome %s, this is protected content"}`, username)
	})

	mux.Handle("/protected", AuthMiddleware(protected))

	fmt.Println("JWT auth service running at http://localhost:8080")
	http.ListenAndServe(":8080", mux)
}
BASH
# Get token
curl -X POST http://localhost:8080/login \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "123456"}'

# Use token to access protected resource
curl -H "Authorization: Bearer <token>" http://localhost:8080/protected

Exercise 3: Build a Concurrency-Safe API Rate Limiter

Requirements:

Reference Solution
GO
package main

import (
	"fmt"
	"net/http"
	"sync"
	"time"
)

// SlidingWindowLimiter sliding window rate limiter
type SlidingWindowLimiter struct {
	mu       sync.Mutex
	windows  map[string]*window
	limit    int
	interval time.Duration
}

type window struct {
	count     int
	startTime time.Time
}

func NewSlidingWindowLimiter(limit int, interval time.Duration) *SlidingWindowLimiter {
	return &SlidingWindowLimiter{
		windows:  make(map[string]*window),
		limit:    limit,
		interval: interval,
	}
}

func (l *SlidingWindowLimiter) Allow(key string) bool {
	l.mu.Lock()
	defer l.mu.Unlock()

	now := time.Now()
	w, exists := l.windows[key]

	if !exists || now.Sub(w.startTime) >= l.interval {
		// Open new window
		l.windows[key] = &window{count: 1, startTime: now}
		return true
	}

	if w.count >= l.limit {
		return false
	}

	w.count++
	return true
}

// Cleanup expired windows (should be called periodically)
func (l *SlidingWindowLimiter) Cleanup() {
	l.mu.Lock()
	defer l.mu.Unlock()

	now := time.Now()
	for key, w := range l.windows {
		if now.Sub(w.startTime) >= l.interval*2 {
			delete(l.windows, key)
		}
	}
}

func SlidingWindowMiddleware(limiter *SlidingWindowLimiter) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if !limiter.Allow(r.RemoteAddr) {
				w.Header().Set("Retry-After", "60")
				http.Error(w, `{"error": "Too many requests"}`, http.StatusTooManyRequests)
				return
			}
			next.ServeHTTP(w, r)
		})
	}
}

func main() {
	limiter := NewSlidingWindowLimiter(5, time.Minute) // 5 per minute

	// Periodic cleanup
	go func() {
		for {
			time.Sleep(10 * time.Minute)
			limiter.Cleanup()
		}
	}()

	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Request successful: %s", time.Now().Format("15:04:05"))
	})

	handler := SlidingWindowMiddleware(limiter)(mux)

	fmt.Println("Sliding window rate limiter running at http://localhost:8080")
	fmt.Println("Rate limit rule: 5 requests per minute")
	http.ListenAndServe(":8080", handler)
}

📚 Next Lesson

Congratulations on completing HTTP programming! In the next lesson, we will learn Go's Testing Programming — including unit tests, table-driven tests, benchmark tests, and more, helping you write more reliable code.

Next Lesson: Testing →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏