Giao diện
🌐 HTTP Services
"HTTP is the lingua franca of the modern web."
Go's `net/http` là một trong những best-designed HTTP libraries trong bất kỳ ngôn ngữ nào.
Go's `net/http` là một trong những best-designed HTTP libraries trong bất kỳ ngôn ngữ nào.
🏗️ HTTP Fundamentals in Go
Basic Server
go
package main
import (
"encoding/json"
"net/http"
)
func main() {
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
http.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
users := []string{"Alice", "Bob"}
json.NewEncoder(w).Encode(users)
})
http.ListenAndServe(":8080", nil)
}Handler Interface
go
// The fundamental building block
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
// HandlerFunc adapter - any func can be a Handler
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}⚔️ Tradeoff: Router Selection
| Router | Performance | Features | When to Use |
|---|---|---|---|
| net/http | ★★★★★ | Basic | Small services, embedded |
| chi | ★★★★★ | Moderate | Most production services |
| gin | ★★★★☆ | Rich | High-traffic APIs |
| echo | ★★★★☆ | Rich | REST APIs |
| gorilla/mux | ★★★☆☆ | Moderate | Legacy (deprecated) |
Recommendation: chi (Idiomatic, Fast, Composable)
go
import "github.com/go-chi/chi/v5"
func main() {
r := chi.NewRouter()
// Middleware
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
// Routes
r.Get("/", homeHandler)
r.Route("/api/v1", func(r chi.Router) {
r.Route("/users", func(r chi.Router) {
r.Get("/", listUsers)
r.Post("/", createUser)
r.Get("/{id}", getUser)
r.Put("/{id}", updateUser)
r.Delete("/{id}", deleteUser)
})
})
http.ListenAndServe(":8080", r)
}🔧 Middleware Patterns
Middleware Chain
go
// Middleware signature
type Middleware func(http.Handler) http.Handler
// Logging middleware
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Wrap ResponseWriter to capture status code
ww := &responseWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(ww, r)
log.Printf("%s %s %d %v",
r.Method, r.URL.Path, ww.status, time.Since(start))
})
}
type responseWriter struct {
http.ResponseWriter
status int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.status = code
rw.ResponseWriter.WriteHeader(code)
}
// Recovery middleware
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("panic: %v\n%s", err, debug.Stack())
http.Error(w, "Internal Server Error", 500)
}
}()
next.ServeHTTP(w, r)
})
}
// Auth middleware
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", 401)
return
}
user, err := validateToken(token)
if err != nil {
http.Error(w, "Invalid token", 401)
return
}
// Add user to context
ctx := context.WithValue(r.Context(), userContextKey, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}Context Propagation
go
type contextKey string
const (
requestIDKey contextKey = "request_id"
userKey contextKey = "user"
)
// Set in middleware
func RequestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reqID := r.Header.Get("X-Request-ID")
if reqID == "" {
reqID = uuid.New().String()
}
ctx := context.WithValue(r.Context(), requestIDKey, reqID)
w.Header().Set("X-Request-ID", reqID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// Get in handler
func GetRequestID(ctx context.Context) string {
if v := ctx.Value(requestIDKey); v != nil {
return v.(string)
}
return ""
}⏱️ Timeouts & Graceful Shutdown
Server Timeouts (Critical!)
go
srv := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 5 * time.Second, // Read request body
WriteTimeout: 10 * time.Second, // Write response
IdleTimeout: 120 * time.Second, // Keep-alive connections
}
// For specific handlers with long operations
func longOperation(w http.ResponseWriter, r *http.Request) {
// Create context with timeout
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
result, err := expensiveOperation(ctx)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
http.Error(w, "timeout", http.StatusGatewayTimeout)
return
}
http.Error(w, err.Error(), 500)
return
}
json.NewEncoder(w).Encode(result)
}Graceful Shutdown
go
func main() {
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
// Start server in goroutine
go func() {
log.Println("Server starting on :8080")
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("ListenAndServe: %v", err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
// Give outstanding requests 30 seconds to complete
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server exited properly")
}📝 Request Validation
JSON Request Handling
go
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=2,max=100"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"gte=0,lte=150"`
}
func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
// Decode JSON
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.respondError(w, http.StatusBadRequest, "invalid JSON")
return
}
// Validate
if err := h.validator.Struct(req); err != nil {
h.respondError(w, http.StatusBadRequest, formatValidationError(err))
return
}
// Process...
user, err := h.service.Create(r.Context(), &req)
if err != nil {
h.handleError(w, err)
return
}
h.respondJSON(w, http.StatusCreated, user)
}🚨 Error Responses (RFC 7807)
Problem Details Standard
go
// Problem Details response (RFC 7807)
type ProblemDetails struct {
Type string `json:"type"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail,omitempty"`
Instance string `json:"instance,omitempty"`
}
func respondError(w http.ResponseWriter, status int, detail string) {
problem := ProblemDetails{
Type: fmt.Sprintf("https://api.example.com/problems/%d", status),
Title: http.StatusText(status),
Status: status,
Detail: detail,
}
w.Header().Set("Content-Type", "application/problem+json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(problem)
}
// Example response:
// {
// "type": "https://api.example.com/problems/400",
// "title": "Bad Request",
// "status": 400,
// "detail": "email: must be a valid email address"
// }
)}}Error Mapping
go
func (h *Handler) handleError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, ErrNotFound):
respondError(w, http.StatusNotFound, err.Error())
case errors.Is(err, ErrConflict):
respondError(w, http.StatusConflict, err.Error())
case errors.Is(err, ErrValidation):
respondError(w, http.StatusBadRequest, err.Error())
case errors.Is(err, context.DeadlineExceeded):
respondError(w, http.StatusGatewayTimeout, "request timeout")
default:
log.Printf("internal error: %v", err)
respondError(w, http.StatusInternalServerError, "internal error")
}
}💻 Engineering Example: Production HTTP API
go
package main
import (
"context"
"encoding/json"
"errors"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-playground/validator/v10"
)
// Handler with dependencies
type UserHandler struct {
service UserService
validator *validator.Validate
logger *log.Logger
}
func NewUserHandler(svc UserService) *UserHandler {
return &UserHandler{
service: svc,
validator: validator.New(),
logger: log.Default(),
}
}
// Response helpers
func (h *UserHandler) respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func (h *UserHandler) respondError(w http.ResponseWriter, status int, message string) {
h.respondJSON(w, status, map[string]string{"error": message})
}
// Handlers
func (h *UserHandler) List(w http.ResponseWriter, r *http.Request) {
users, err := h.service.List(r.Context())
if err != nil {
h.logger.Printf("list users error: %v", err)
h.respondError(w, http.StatusInternalServerError, "failed to list users")
return
}
h.respondJSON(w, http.StatusOK, users)
}
func (h *UserHandler) Get(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
user, err := h.service.GetByID(r.Context(), id)
if err != nil {
if errors.Is(err, ErrNotFound) {
h.respondError(w, http.StatusNotFound, "user not found")
return
}
h.logger.Printf("get user error: %v", err)
h.respondError(w, http.StatusInternalServerError, "failed to get user")
return
}
h.respondJSON(w, http.StatusOK, user)
}
func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.respondError(w, http.StatusBadRequest, "invalid JSON")
return
}
if err := h.validator.Struct(req); err != nil {
h.respondError(w, http.StatusBadRequest, err.Error())
return
}
user, err := h.service.Create(r.Context(), &req)
if err != nil {
h.logger.Printf("create user error: %v", err)
h.respondError(w, http.StatusInternalServerError, "failed to create user")
return
}
h.respondJSON(w, http.StatusCreated, user)
}
// Router setup
func NewRouter(userHandler *UserHandler) chi.Router {
r := chi.NewRouter()
// Global middleware
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(30 * time.Second))
// CORS (if needed)
r.Use(func(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", "Accept, Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
})
// Health check
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
})
// API routes
r.Route("/api/v1", func(r chi.Router) {
r.Route("/users", func(r chi.Router) {
r.Get("/", userHandler.List)
r.Post("/", userHandler.Create)
r.Get("/{id}", userHandler.Get)
})
})
return r
}
// Main with graceful shutdown
func main() {
// Initialize dependencies...
userHandler := NewUserHandler(userService)
router := NewRouter(userHandler)
srv := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
// Graceful shutdown
go func() {
log.Println("Server starting on :8080")
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("ListenAndServe: %v", 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)
log.Println("Server exited")
}✅ Ship-to-Prod Checklist
Server Configuration
- [ ] Timeouts set: ReadTimeout, WriteTimeout, IdleTimeout
- [ ] Graceful shutdown implemented
- [ ] Health endpoint at
/healthor/healthz - [ ] Ready endpoint at
/ready(if using K8s)
Middleware Stack
- [ ] Logging middleware
- [ ] Recovery middleware (panic handling)
- [ ] Request ID propagation
- [ ] CORS configured (if needed)
- [ ] Auth middleware (if needed)
Request/Response
- [ ] Content-Type headers set correctly
- [ ] Proper status codes returned
- [ ] Error responses follow consistent format
- [ ] Request validation implemented
Security
- [ ] No sensitive data in logs
- [ ] Rate limiting considered
- [ ] Input sanitization
- [ ] TLS in production (or behind proxy)
📊 Summary
| Component | Recommendation |
|---|---|
| Router | chi for most cases |
| Timeouts | Always configure |
| Shutdown | Graceful with 30s deadline |
| Errors | RFC 7807 Problem Details |
| Middleware | Chain: Logger → Recovery → Auth |
➡️ Tiếp theo
HTTP Services nắm vững rồi! Tiếp theo: Database Patterns - Production database access patterns.