HTTP Programming
Lesson 22: HTTP Programming
🎯 Life Analogy
Imagine you run a restaurant:
- HTTP Server is like the restaurant's receptionist — when customers (clients) arrive, the receptionist directs them to the right service window based on their needs
- Routing (ServeMux) is like the restaurant's menu categories — different dishes (URL paths) are handled by different chefs (handler functions)
- Handler Interface is like the chef's work standards — regardless of what dish is being prepared, they must follow a unified workflow
- Middleware is like the restaurant's service process — customers take off their shoes, wash their hands, then sit down; these steps are common to all and not specific to any particular dish
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))
}
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))
}
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())
}
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:
HandleFunc: Accepts a function of typefunc(ResponseWriter, *Request)Handle: Accepts a type that implements theHandlerinterfaceHandleFuncinternally wraps the function into aHandlerFuncadapter
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
Q4: Why is it recommended to pass a custom Mux as the second parameter to ListenAndServe?
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:
DefaultServeMuxis global — any package can register routes, which may cause conflicts- Custom Mux has controllable scope, avoiding unexpected route overrides
- 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:
- Create a
POST /uploadendpoint to receive file uploads - Limit file size to 10MB
- Only allow image types (jpg, png, gif)
- Save files to the
./uploadsdirectory - Return the file's access URL
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:
- Implement simple JWT generation and verification (can simulate with Base64)
- Create a
POST /loginendpoint to obtain a token - Create a protected
GET /protectedendpoint - Pass token via
Authorization: Bearer <token>
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:
- Implement sliding window rate limiting algorithm
- Support configuring max requests per window and window size
- Provide middleware interface
- Use Redis or in-memory storage (in-memory is fine)
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.



