Why This Matters - Choosing Your Development Foundation
Think of web frameworks as specialized toolkits for building houses. You could build a house with just a hammer and saw, but frameworks give you pre-built walls, plumbing systems, and electrical wiring that save enormous amounts of time and ensure everything works together properly.
Real-World Impact: The choice of web framework can make or break your project. A well-chosen framework accelerates development, simplifies maintenance, and scales with your business. A poor choice leads to technical debt, slow performance, and difficulty hiring developers.
The Strategic Decision: Web frameworks shape your entire development experience:
- Productivity: Frameworks reduce boilerplate code by 60-80%
- Performance: Optimized routing and middleware can handle 10-40x more requests
- Ecosystem: Access to middleware, monitoring, and deployment tools
- Team Success: Learning curves affect onboarding and development speed
- Business Risk: Vendor lock-in vs. flexibility impacts long-term options
Learning Objectives
By the end of this guide, you will be able to:
π― Framework Selection: Choose the right framework based on project requirements, team skills, and business constraints
ποΈ Implementation Skills: Set up production-ready applications using any major Go web framework
π§ Pattern Mastery: Implement common patterns like authentication, middleware, error handling, and testing across frameworks
π Decision Framework: Evaluate trade-offs between performance, compatibility, and ecosystem support
π Production Readiness: Deploy scalable, secure web applications with proper monitoring and observability
Core Concepts - Understanding Web Framework Architecture
Web frameworks are not magicβthey're well-designed abstractions over Go's standard net/http package. Understanding these core concepts helps you make informed decisions.
The HTTP Handler Abstraction
Standard Library Approach:
1// Standard HTTP handler - low level control, lots of boilerplate
2func standardHandler(w http.ResponseWriter, r *http.Request) {
3 // Manual parameter parsing
4 query := r.URL.Query()
5 id := query.Get("id")
6
7 // Manual JSON handling
8 var user User
9 if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
10 http.Error(w, err.Error(), http.StatusBadRequest)
11 return
12 }
13
14 // Manual response
15 w.Header().Set("Content-Type", "application/json")
16 json.NewEncoder(w).Encode(map[string]interface{}{
17 "id": id,
18 "user": user,
19 })
20}
Framework Approach:
1// Framework handler - high productivity, less boilerplate
2func frameworkHandler(c *gin.Context) {
3 // Automatic parameter parsing
4 id := c.Param("id")
5
6 // Automatic JSON binding with validation
7 var user User
8 if err := c.ShouldBindJSON(&user); err != nil {
9 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
10 return
11 }
12
13 // Automatic JSON response
14 c.JSON(http.StatusOK, gin.H{
15 "id": id,
16 "user": user,
17 })
18}
Key Framework Components
1. Router: Efficient URL pattern matching and request dispatching
2. Context: Rich request/response abstraction with convenience methods
3. Middleware: Composable request processing pipeline
4. Validation: Automatic request validation and error handling
5. Rendering: JSON, XML, HTML, and template rendering helpers
The Compatibility Spectrum
Frameworks exist on a spectrum from pure net/http compatibility to complete custom implementations:
Standard Library ββ Chi ββ Gin/Echo ββ Fiber
(net/http compatible)
- Left Side: Maximum compatibility, standard Go patterns
- Right Side: Maximum performance, learning curve, ecosystem lock-in
Practical Examples - Framework Comparison in Action
Let's explore the four major Go web frameworks through practical examples that demonstrate their strengths and trade-offs.
Framework 1: Chi - The Standard Library Champion
Chi embraces net/http compatibility, making it ideal for gradual adoption and enterprise environments.
1// run
2package main
3
4import (
5 "context"
6 "fmt"
7 "net/http"
8 "time"
9
10 "github.com/go-chi/chi/v5"
11 "github.com/go-chi/chi/v5/middleware"
12)
13
14func main() {
15 // Create chi router with standard net/http compatibility
16 r := chi.NewRouter()
17
18 // Standard middleware that works with any net/http handler
19 r.Use(middleware.Logger)
20 r.Use(middleware.Recoverer)
21 r.Use(middleware.Timeout(60 * time.Second))
22
23 // Simple route - identical to standard library
24 r.Get("/", func(w http.ResponseWriter, r *http.Request) {
25 w.Header().Set("Content-Type", "application/json")
26 json.NewEncoder(w).Encode(map[string]string{
27 "message": "Hello from Chi!",
28 "framework": "net/http compatible",
29 })
30 })
31
32 // Route with path parameter
33 r.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
34 id := chi.URLParam(r, "id")
35
36 w.Header().Set("Content-Type", "application/json")
37 json.NewEncoder(w).Encode(map[string]interface{}{
38 "user_id": id,
39 "name": "John Doe",
40 })
41 })
42
43 // Route groups for organization
44 r.Route("/api/v1", func(r chi.Router) {
45 r.Get("/users", getUsers)
46 r.Post("/users", createUser)
47 r.Put("/users/{id}", updateUser)
48 r.Delete("/users/{id}", deleteUser)
49 })
50
51 // Custom middleware example
52 r.Use(corsMiddleware)
53
54 fmt.Println("Chi server starting on :8080")
55 http.ListenAndServe(":8080", r)
56}
57
58// corsMiddleware demonstrates chi's compatibility with standard middleware
59func corsMiddleware(next http.Handler) http.Handler {
60 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
61 w.Header().Set("Access-Control-Allow-Origin", "*")
62 w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
63 w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
64
65 if r.Method == "OPTIONS" {
66 w.WriteHeader(http.StatusOK)
67 return
68 }
69
70 next.ServeHTTP(w, r)
71 })
72}
73
74type User struct {
75 ID string `json:"id"`
76 Name string `json:"name"`
77 Email string `json:"email"`
78}
79
80func getUsers(w http.ResponseWriter, r *http.Request) {
81 users := []User{
82 {ID: "1", Name: "Alice", Email: "alice@example.com"},
83 {ID: "2", Name: "Bob", Email: "bob@example.com"},
84 }
85
86 w.Header().Set("Content-Type", "application/json")
87 json.NewEncoder(w).Encode(users)
88}
89
90func createUser(w http.ResponseWriter, r *http.Request) {
91 var user User
92 if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
93 http.Error(w, err.Error(), http.StatusBadRequest)
94 return
95 }
96
97 user.ID = "3"
98 w.Header().Set("Content-Type", "application/json")
99 w.WriteHeader(http.StatusCreated)
100 json.NewEncoder(w).Encode(user)
101}
102
103func updateUser(w http.ResponseWriter, r *http.Request) {
104 id := chi.URLParam(r, "id")
105 var user User
106 if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
107 http.Error(w, err.Error(), http.StatusBadRequest)
108 return
109 }
110
111 user.ID = id
112 w.Header().Set("Content-Type", "application/json")
113 json.NewEncoder(w).Encode(user)
114}
115
116func deleteUser(w http.ResponseWriter, r *http.Request) {
117 id := chi.URLParam(r, "id")
118
119 w.Header().Set("Content-Type", "application/json")
120 json.NewEncoder(w).Encode(map[string]interface{}{
121 "message": "User deleted",
122 "id": id,
123 })
124}
Chi Strengths:
- 100% net/http compatibility - works with all standard Go middleware
- Minimal dependencies - small binary size and fast compilation
- Standard Go patterns - feels natural to experienced Go developers
- Enterprise-friendly - fits well with existing Go infrastructure
Chi Trade-offs:
- Less "magic" than other frameworks - more manual work
- Smaller built-in middleware ecosystem
- Slower routing performance than custom implementations
Framework 2: Gin - The Productivity Powerhouse
Gin provides the perfect balance between performance and developer productivity with Express.js-inspired APIs.
1// run
2package main
3
4import (
5 "net/http"
6 "strconv"
7 "time"
8
9 "github.com/gin-gonic/gin"
10 "github.com/gin-contrib/cors"
11)
12
13func main() {
14 // Create Gin router with default middleware
15 r := gin.Default() // Includes Logger and Recovery
16
17 // Add CORS middleware
18 r.Use(cors.New(cors.Config{
19 AllowOrigins: []string{"https://example.com"},
20 AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
21 AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
22 ExposeHeaders: []string{"Content-Length"},
23 AllowCredentials: true,
24 MaxAge: 12 * time.Hour,
25 }))
26
27 // Simple route with automatic JSON response
28 r.GET("/", func(c *gin.Context) {
29 c.JSON(http.StatusOK, gin.H{
30 "message": "Hello from Gin!",
31 "framework": "Express.js inspired",
32 })
33 })
34
35 // Route with parameter binding and validation
36 r.POST("/users", func(c *gin.Context) {
37 var user User
38 if err := c.ShouldBindJSON(&user); err != nil {
39 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
40 return
41 }
42
43 // Validate user data
44 if user.Name == "" {
45 c.JSON(http.StatusBadRequest, gin.H{"error": "Name is required"})
46 return
47 }
48
49 if user.Age < 18 {
50 c.JSON(http.StatusBadRequest, gin.H{"error": "Must be 18 or older"})
51 return
52 }
53
54 user.ID = "123"
55 c.JSON(http.StatusCreated, user)
56 })
57
58 // Route with query parameters
59 r.GET("/search", func(c *gin.Context) {
60 query := c.DefaultQuery("q", "")
61 page := c.DefaultQuery("page", "1")
62 limit := c.DefaultQuery("limit", "10")
63
64 pageInt, _ := strconv.Atoi(page)
65 limitInt, _ := strconv.Atoi(limit)
66
67 c.JSON(http.StatusOK, gin.H{
68 "query": query,
69 "page": pageInt,
70 "limit": limitInt,
71 "results": []string{"Result 1", "Result 2", "Result 3"},
72 })
73 })
74
75 // Route groups for API versioning
76 v1 := r.Group("/api/v1")
77 {
78 v1.GET("/users", func(c *gin.Context) {
79 c.JSON(http.StatusOK, []User{
80 {ID: "1", Name: "Alice", Email: "alice@example.com"},
81 {ID: "2", Name: "Bob", Email: "bob@example.com"},
82 })
83 })
84
85 // Protected routes with middleware
86 auth := v1.Group("/")
87 auth.Use(authMiddleware())
88 {
89 auth.GET("/profile", func(c *gin.Context) {
90 userID := c.MustGet("user_id").(string)
91 c.JSON(http.StatusOK, gin.H{
92 "user_id": userID,
93 "profile": "User profile data",
94 })
95 })
96 }
97 })
98
99 // File upload handling
100 r.POST("/upload", func(c *gin.Context) {
101 file, err := c.FormFile("file")
102 if err != nil {
103 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
104 return
105 }
106
107 // Validate file size
108 if file.Size > 10*1024*1024 { // 10MB
109 c.JSON(http.StatusBadRequest, gin.H{"error": "File too large"})
110 return
111 }
112
113 // Save file
114 dst := "./uploads/" + file.Filename
115 if err := c.SaveUploadedFile(file, dst); err != nil {
116 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
117 return
118 }
119
120 c.JSON(http.StatusOK, gin.H{
121 "message": "File uploaded successfully",
122 "filename": file.Filename,
123 "size": file.Size,
124 })
125 })
126
127 r.Run(":8080")
128}
129
130// User model with validation tags
131type User struct {
132 ID string `json:"id"`
133 Name string `json:"name" binding:"required"`
134 Email string `json:"email" binding:"required,email"`
135 Age int `json:"age" binding:"required,gte=18"`
136}
137
138// authMiddleware demonstrates Gin's custom context
139func authMiddleware() gin.HandlerFunc {
140 return func(c *gin.Context) {
141 token := c.GetHeader("Authorization")
142 if token == "" {
143 c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing token"})
144 c.Abort()
145 return
146 }
147
148 // Simplified token validation
149 if token != "Bearer valid-token" {
150 c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
151 c.Abort()
152 return
153 }
154
155 // Set user info in context
156 c.Set("user_id", "12345")
157 c.Next()
158 }
159}
Gin Strengths:
- Excellent performance
- Rich middleware ecosystem with battle-tested components
- Automatic JSON binding and validation reduces boilerplate
- Large community and extensive documentation
Gin Trade-offs:
- Custom context creates vendor lock-in
- Can't use standard net/http middleware without adapters
- Learning curve for developers unfamiliar with Express.js patterns
Framework 3: Echo - The Minimalist Performer
Echo focuses on clean API design and performance while maintaining flexibility.
1// run
2package main
3
4import (
5 "net/http"
6 "strconv"
7 "time"
8
9 "github.com/labstack/echo/v4"
10 "github.com/labstack/echo/v4/middleware"
11 "github.com/go-playground/validator/v10"
12)
13
14func main() {
15 // Create Echo instance
16 e := echo.New()
17
18 // Hide Echo banner
19 e.HideBanner = true
20
21 // Add middleware
22 e.Use(middleware.Logger())
23 e.Use(middleware.Recover())
24 e.Use(middleware.CORS())
25 e.Use(middleware.Gzip())
26
27 // Custom validator
28 e.Validator = &CustomValidator{validator: validator.New()}
29
30 // Simple route
31 e.GET("/", func(c echo.Context) error {
32 return c.JSON(http.StatusOK, map[string]string{
33 "message": "Hello from Echo!",
34 "framework": "Minimalist & Fast",
35 })
36 })
37
38 // Route with validation
39 e.POST("/users", createUser)
40
41 // Route groups with middleware
42 api := e.Group("/api")
43 api.Use(middleware.KeyAuth(func(username, password string, c echo.Context) {
44 return username == "admin" && password == "secret", nil
45 }))
46 {
47 api.GET("/users", func(c echo.Context) error {
48 users := []User{
49 {ID: 1, Name: "Alice", Email: "alice@example.com"},
50 {ID: 2, Name: "Bob", Email: "bob@example.com"},
51 }
52 return c.JSON(http.StatusOK, users)
53 })
54 }
55
56 // WebSocket support
57 e.GET("/ws", func(c echo.Context) error {
58 return echo.NewHTTPError(http.StatusNotImplemented, "WebSocket example")
59 })
60
61 // Start server
62 e.Logger.Fatal(e.Start(":8080"))
63}
64
65type User struct {
66 ID int `json:"id" validate:"required"`
67 Name string `json:"name" validate:"required,min=3,max=50"`
68 Email string `json:"email" validate:"required,email"`
69}
70
71// Custom validator implements echo.Validator
72type CustomValidator struct {
73 validator *validator.Validate
74}
75
76func Validate(i interface{}) error {
77 if err := cv.validator.Struct(i); err != nil {
78 return echo.NewHTTPError(http.StatusBadRequest, err.Error())
79 }
80 return nil
81}
82
83func createUser(c echo.Context) error {
84 u := new(User)
85 if err := c.Bind(u); err != nil {
86 return echo.NewHTTPError(http.StatusBadRequest, err.Error())
87 }
88
89 if err := c.Validate(u); err != nil {
90 return err
91 }
92
93 u.ID = 3
94 return c.JSON(http.StatusCreated, u)
95}
96
97// Server-Sent Events example
98func streamHandler(c echo.Context) error {
99 c.Response().Header().Set(echo.HeaderContentType, "text/event-stream")
100 c.Response().Header().Set("Cache-Control", "no-cache")
101 c.Response().Header().Set("Connection", "keep-alive")
102 c.Response().WriteHeader(http.StatusOK)
103
104 for i := 0; i < 5; i++ {
105 fmt.Fprintf(c.Response(), "data: Message %d\n\n", i+1)
106 c.Response().Flush()
107 time.Sleep(1 * time.Second)
108 }
109
110 return nil
111}
Echo Strengths:
- Clean, minimal API design
- Excellent performance with low memory allocation
- Built-in TLS and HTTP/2 support
- Flexible middleware system
- WebSocket and Server-Sent Events support
Echo Trade-offs:
- Smaller community than Gin
- Less built-in middleware available
- Custom context like Gin
Framework 4: Fiber - The Ultimate Performer
Fiber uses fasthttp for extreme performance, trading some compatibility for speed.
1// run
2package main
3
4import (
5 "strconv"
6 "time"
7
8 "github.com/gofiber/fiber/v2"
9 "github.com/gofiber/fiber/v2/middleware"
10 "github.com/gofiber/fiber/v2/utils"
11)
12
13func main() {
14 // Create Fiber app
15 app := fiber.New(fiber.Config{
16 AppName: "Fiber App",
17 ReadTimeout: 10 * time.Second,
18 WriteTimeout: 10 * time.Second,
19 IdleTimeout: 30 * time.Second,
20 CaseSensitive: true,
21 StrictRouting: true,
22 })
23
24 // Add middleware
25 app.Use(middleware.Logger())
26 app.Use(middleware.Recover())
27 app.Use(middleware.CORS())
28 app.Use(middleware.Compress())
29
30 // Simple route
31 app.Get("/", func(c *fiber.Ctx) error {
32 return c.JSON(fiber.Map{
33 "message": "Hello from Fiber!",
34 "framework": "Ultimate Performance",
35 })
36 })
37
38 // Route with parameter handling
39 app.Get("/users/:id", func(c *fiber.Ctx) error {
40 id := c.Params("id")
41
42 return c.JSON(fiber.Map{
43 "user_id": id,
44 "name": "John Doe",
45 "email": "john@example.com",
46 })
47 })
48
49 // Route with query parameters
50 app.Get("/search", func(c *fiber.Ctx) error {
51 query := c.Query("q")
52 page, _ := strconv.Atoi(c.Query("page", "1"))
53 limit, _ := strconv.Atoi(c.Query("limit", "10"))
54
55 return c.JSON(fiber.Map{
56 "query": query,
57 "page": page,
58 "limit": limit,
59 "results": []string{"Result 1", "Result 2", "Result 3"},
60 })
61 })
62
63 // File upload with streaming
64 app.Post("/upload", func(c *fiber.Ctx) error {
65 file, err := c.FormFile("document")
66 if err != nil {
67 return c.Status(fiber.StatusBadRequest).SendString(err.Error())
68 }
69
70 // Get file info
71 fileInfo, err := file.Header.FormFile("document")
72 if err != nil {
73 return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
74 }
75
76 // Validate file
77 if fileInfo.Size > 50*1024*1024 { // 50MB
78 return c.Status(fiber.StatusBadRequest).SendString("File too large")
79 }
80
81 // Save file
82 err = c.SaveFile(file, "./uploads/"+file.Filename)
83 if err != nil {
84 return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
85 }
86
87 return c.JSON(fiber.Map{
88 "message": "File uploaded successfully",
89 "filename": file.Filename,
90 "size": fileInfo.Size,
91 })
92 })
93
94 // WebSocket support
95 app.Get("/ws", func(c *fiber.Ctx) error {
96 if websocket.IsWebSocketUpgrade(c) {
97 return websocket.New(func(c *websocket.Conn) {
98 for {
99 mt, msg, err := c.ReadMessage()
100 if err != nil {
101 break
102 }
103 if mt == websocket.TextMessage {
104 log.Printf("Received: %s", msg)
105 c.WriteMessage(websocket.TextMessage, "Echo: "+msg)
106 }
107 }
108 })
109 }
110 return fiber.ErrNotImplemented
111 })
112
113 // Static file serving
114 app.Static("/", "./public")
115
116 log.Println("Fiber server starting on :8080")
117 app.Listen(":8080")
118}
Fiber Strengths:
- Extremely fast
- Low memory usage and allocations
- Express.js compatibility for Node.js developers
- Built-in WebSocket and static file serving
Fiber Trade-offs:
- Uses fasthttp instead of net/http - incompatibility with standard Go libraries
- Different API semantics from standard Go
- Smaller ecosystem and community
- Vendor lock-in to fasthttp
Common Patterns and Pitfalls - Production Experience
Pattern 1: Middleware Composition
Problem: Cross-cutting concerns like logging, authentication, and rate limiting should be reusable across all routes.
Solution: Build composable middleware that works consistently across your chosen framework.
1// Framework-agnostic middleware pattern
2type Middleware func(http.Handler) http.Handler
3
4func Chain(middlewares ...Middleware) Middleware {
5 return func(final http.Handler) http.Handler {
6 for i := len(middlewares) - 1; i >= 0; i-- {
7 final = middlewares[i](final)
8 }
9 return final
10 }
11}
12
13// Rate limiting middleware
14func RateLimit(requests int, window time.Duration) Middleware {
15 limiter := NewRateLimiter(requests, window)
16
17 return func(next http.Handler) http.Handler {
18 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19 if !limiter.Allow(r.RemoteAddr) {
20 http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
21 return
22 }
23 next.ServeHTTP(w, r)
24 })
25 }
26}
Pattern 2: Configuration Management
Problem: Production applications need different configurations for development, staging, and production environments.
Solution: Implement environment-based configuration with validation.
1// Production-ready configuration management
2type Config struct {
3 Server struct {
4 Host string `env:"SERVER_HOST" envDefault:"0.0.0.0"`
5 Port int `env:"SERVER_PORT" envDefault:"8080"`
6 ReadTimeout time.Duration `env:"SERVER_READ_TIMEOUT" envDefault:"30s"`
7 WriteTimeout time.Duration `env:"SERVER_WRITE_TIMEOUT" envDefault:"30s"`
8 } `json:"server"`
9
10 Database struct {
11 Host string `env:"DB_HOST" envDefault:"localhost"`
12 Port int `env:"DB_PORT" envDefault:"5432"`
13 Name string `env:"DB_NAME" envDefault:"myapp"`
14 User string `env:"DB_USER" envDefault:"user"`
15 Password string `env:"DB_PASSWORD" envDefault:"password"`
16 SSLMode string `env:"DB_SSL_MODE" envDefault:"disable"`
17 } `json:"database"`
18
19 JWT struct {
20 Secret string `env:"JWT_SECRET"`
21 Expiration string `env:"JWT_EXPIRATION" envDefault:"24h"`
22 } `json:"jwt"`
23
24 LogLevel string `env:"LOG_LEVEL" envDefault:"info"`
25}
26
27func LoadConfig() {
28 var config Config
29
30 // Load from environment with defaults
31 err := env.Parse(&config)
32 if err != nil {
33 return nil, fmt.Errorf("failed to parse config: %w", err)
34 }
35
36 // Validate configuration
37 if err := config.Validate(); err != nil {
38 return nil, fmt.Errorf("invalid config: %w", err)
39 }
40
41 return &config, nil
42}
43
44func Validate() error {
45 if c.JWT.Secret == "" {
46 return fmt.Errorf("JWT_SECRET is required")
47 }
48
49 if c.Server.Port < 1 || c.Server.Port > 65535 {
50 return fmt.Errorf("invalid port: %d", c.Server.Port)
51 }
52
53 return nil
54}
Common Pitfalls to Avoid
β Choosing Framework Based on Benchmarks Alone: Real-world bottlenecks are usually databases, not routing. Compatibility and ecosystem often matter more.
β Ignoring Team Experience: A framework that looks great on paper but nobody on your team knows can slow down development dramatically.
β Underestimating Middleware Needs: Authentication, logging, rate limiting, and CORS are essential for production applications.
β Forgetting About Long-term Maintenance: Consider who will maintain this code in 5 years and whether the framework will still be supported.
β Not Planning for Scaling: Think about horizontal scaling, database connection pooling, and caching from the beginning.
β Security Afterthought: Always implement security headers, input validation, and authentication from day one.
Integration and Mastery - Building Production Systems
Real-World Example: Microservice API Gateway
Let's build a complete microservice API gateway that demonstrates how frameworks handle real-world production requirements.
1// run
2package main
3
4import (
5 "context"
6 "encoding/json"
7 "fmt"
8 "net/http"
9 "time"
10
11 "github.com/gin-gonic/gin"
12 "github.com/gin-contrib/cors"
13)
14
15type APIServer struct {
16 config *Config
17 services map[string]ServiceClient
18 rateLimiter *RateLimiter
19 logger *Logger
20}
21
22type Config struct {
23 Port int `json:"port"`
24 Services []ServiceConfig `json:"services"`
25 RateLimit RateLimitConfig `json:"rate_limit"`
26}
27
28type ServiceConfig struct {
29 Name string `json:"name"`
30 URL string `json:"url"`
31}
32
33type RateLimitConfig struct {
34 Requests int `json:"requests"`
35 Window time.Duration `json:"window"`
36}
37
38type ServiceClient struct {
39 URL string
40 Client *http.Client
41}
42
43type ProxyRequest struct {
44 Service string `json:"service"`
45 Path string `json:"path"`
46 Method string `json:"method"`
47 Headers map[string]string `json:"headers"`
48 Body interface{} `json:"body"`
49}
50
51func NewAPIServer(config *Config) *APIServer {
52 // Initialize service clients
53 services := make(map[string]ServiceClient)
54 for _, service := range config.Services {
55 services[service.Name] = ServiceClient{
56 URL: service.URL,
57 Client: &http.Client{
58 Timeout: 30 * time.Second,
59 },
60 }
61 }
62
63 return &APIServer{
64 config: config,
65 services: services,
66 rateLimiter: NewRateLimiter(config.RateLimit.Requests, config.RateLimit.Window),
67 logger: NewLogger(),
68 }
69}
70
71func setupRouter() *gin.Engine {
72 r := gin.New()
73
74 // Add CORS middleware
75 r.Use(cors.New(cors.Config{
76 AllowOrigins: []string{"*"},
77 AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
78 AllowHeaders: []string{"*"},
79 ExposeHeaders: []string{"Content-Length"},
80 MaxAge: 12 * time.Hour,
81 }))
82
83 // Add request ID middleware
84 r.Use(requestIDMiddleware)
85
86 // Add rate limiting middleware
87 r.Use(s.rateLimiterMiddleware())
88
89 // Add logging middleware
90 r.Use(s.loggingMiddleware())
91
92 // Health check
93 r.GET("/health", s.healthCheck)
94
95 // Proxy routes
96 r.Any("/*path", s.proxyHandler)
97
98 return r
99}
100
101func rateLimiterMiddleware() gin.HandlerFunc {
102 return func(c *gin.Context) {
103 clientIP := c.ClientIP()
104 if !s.rateLimiter.Allow(clientIP) {
105 c.JSON(http.StatusTooManyRequests, gin.H{
106 "error": "Rate limit exceeded",
107 })
108 c.Abort()
109 return
110 }
111 c.Next()
112 }
113}
114
115func loggingMiddleware() gin.HandlerFunc {
116 return func(c *gin.Context) {
117 start := time.Now()
118 path := c.Request.URL.Path
119 method := c.Request.Method
120 requestID := c.GetHeader("X-Request-ID")
121
122 c.Next()
123
124 duration := time.Since(start)
125 statusCode := c.Writer.Status()
126
127 s.logger.Info("Request completed",
128 "request_id", requestID,
129 "method", method,
130 "path", path,
131 "status", statusCode,
132 "duration", duration,
133 )
134 }
135}
136
137func proxyHandler(c *gin.Context) {
138 path := c.Param("path")
139 method := c.Request.Method
140
141 // Determine target service based on path
142 serviceName := s.determineService(path)
143 service, exists := s.services[serviceName]
144 if !exists {
145 c.JSON(http.StatusNotFound, gin.H{
146 "error": "Service not found",
147 "service": serviceName,
148 })
149 return
150 }
151
152 // Build proxy request
153 proxyPath := s.buildProxyPath(serviceName, path)
154 proxyURL := service.URL + proxyPath
155
156 // Read request body
157 var body interface{}
158 if c.Request.Body != nil {
159 if err := c.ShouldBindJSON(&body); err != nil {
160 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
161 return
162 }
163 }
164
165 // Create HTTP request
166 req, err := http.NewRequest(method, proxyURL, nil)
167 if err != nil {
168 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
169 return
170 }
171
172 // Copy headers
173 for name, values := range c.Request.Header {
174 if !s.shouldSkipHeader(name) {
175 for _, value := range values {
176 req.Header.Add(name, value)
177 }
178 }
179 }
180
181 // Set body if present
182 if body != nil {
183 jsonBody, _ := json.Marshal(body)
184 req.Body = bytes.NewReader(jsonBody)
185 req.Header.Set("Content-Type", "application/json")
186 }
187
188 // Make request to downstream service
189 resp, err := service.Client.Do(req)
190 if err != nil {
191 s.logger.Error("Proxy request failed", "error", err)
192 c.JSON(http.StatusBadGateway, gin.H{
193 "error": "Service unavailable",
194 })
195 return
196 }
197
198 defer resp.Body.Close()
199
200 // Copy response headers
201 for name, values := range resp.Header {
202 if !s.shouldSkipHeader(name) {
203 c.Header(name, values[0])
204 }
205 }
206
207 // Set status code
208 c.Status(resp.StatusCode)
209
210 // Stream response body
211 io.Copy(c.Writer, resp.Body)
212}
213
214func healthCheck(c *gin.Context) {
215 status := "healthy"
216 services := make(map[string]interface{})
217
218 // Check all downstream services
219 for name, service := range s.services {
220 resp, err := service.Client.Get(service.URL + "/health")
221 if err != nil || resp.StatusCode != http.StatusOK {
222 status = "unhealthy"
223 services[name] = "unhealthy"
224 } else {
225 services[name] = "healthy"
226 }
227 }
228
229 c.JSON(http.StatusOK, gin.H{
230 "status": status,
231 "services": services,
232 "timestamp": time.Now(),
233 })
234}
235
236func determineService(path string) string {
237 // Simple routing logic - in production, use more sophisticated routing
238 if len(path) > 0 {
239 return path[:1] // First character determines service
240 }
241 return "default"
242}
243
244func buildProxyPath(serviceName, path string) string {
245 // Remove service prefix from path
246 if len(path) > 1 {
247 return path[1:]
248 }
249 return "/"
250}
251
252func shouldSkipHeader(header string) bool {
253 skipHeaders := []string{
254 "Connection",
255 "Transfer-Encoding",
256 "Host",
257 "Accept-Encoding",
258 "Origin",
259 }
260
261 for _, skip := range skipHeaders {
262 if header == skip {
263 return true
264 }
265 }
266 return false
267}
268
269func Start() error {
270 r := s.setupRouter()
271
272 s.logger.Info("API server starting",
273 "port", s.config.Port,
274 "services", len(s.config.Services),
275 )
276
277 return r.Run(fmt.Sprintf(":%d", s.config.Port))
278}
279
280func main() {
281 config := &Config{
282 Port: 8080,
283 Services: []ServiceConfig{
284 {Name: "a", URL: "http://localhost:8081"},
285 {Name: "b", URL: "http://localhost:8082"},
286 {Name: "c", URL: "http://localhost:8083"},
287 },
288 RateLimit: RateLimitConfig{
289 Requests: 100,
290 Window: time.Minute,
291 },
292 }
293
294 server := NewAPIServer(config)
295 if err := server.Start(); err != nil {
296 panic(err)
297 }
298}
Practice Exercises
Exercise 1: Framework Selection Analysis
Learning Objective: Analyze project requirements and match them to appropriate web frameworks. Evaluate trade-offs between performance, ecosystem compatibility, and development speed.
Real-World Context: A senior developer at a startup needs to choose frameworks for four different products. Wrong choices could lead to performance issues, slow development, or integration problems.
Difficulty: Beginner | Time Estimate: 30-45 minutes
Scenario: You're a tech lead responsible for selecting web frameworks for four different applications. Each application has unique requirements that will impact your framework choice.
Applications:
- E-commerce API: High traffic, needs authentication, logging, monitoring, and payment gateway integration
- Internal Dashboard: Simple CRUD operations, small team, quick development needed, low maintenance priority
- Real-time Analytics Platform: Must handle 100K+ requests/second, minimal external integrations, performance-critical
- Enterprise Web Application: Long-term maintenance, strict compliance requirements, existing Go infrastructure, security audit required
Task: For each application, select the best framework and write a comprehensive justification considering:
- Performance requirements and expected load
- Team experience and learning curve
- Ecosystem compatibility and third-party integrations
- Long-term maintenance and vendor lock-in concerns
- Security and compliance requirements
Click to see solution
1. E-commerce API: Gin
- Reasoning: Good balance of performance and ecosystem support
- Benefits: Rich middleware ecosystem, large community, good documentation
- Why not others:
- Fiber: Payment gateway libraries may not be compatible with fasthttp
- Echo: Smaller ecosystem than Gin
- Chi: Lower performance might be an issue for high traffic
2. Internal Dashboard: Gin or Echo
- Reasoning: Quick development, good productivity features
- Benefits: Built-in validation, good middleware, easy to learn
- Why not others:
- Fiber: Overkill for simple CRUD operations
- Chi: More boilerplate code needed
3. Real-time Analytics Platform: Fiber
- Reasoning: Performance is critical, requirements are simple
- Benefits: Fastest performance, minimal overhead
- Why not others:
- Gin/Echo: Not fast enough for 100K+ req/s
- Chi: Too slow for this use case
4. Enterprise Web Application: Chi
- Reasoning: Long-term maintenance and compatibility
- Benefits: Full net/http compatibility, standard Go patterns, minimal dependencies
- Why not others:
- Gin/Echo: Custom contexts create vendor lock-in
- Fiber: fasthttp incompatibility issues with enterprise tools
Exercise 2: Production-Ready Project Structure
Learning Objective: Design scalable project directory layouts following Go best practices. Implement configuration management for different environments. Create structured logging with proper log levels. Set up Docker containerization for microservices. Establish error handling patterns that work across frameworks.
Real-World Context: A fintech startup is building a user management API that must pass security audits and handle sensitive data. Proper project structure is crucial for maintainability and compliance.
Difficulty: Intermediate | Time Estimate: 60-90 minutes
Task: Create a production-ready project structure for a web application that follows enterprise standards and Go best practices.
Core Requirements:
- Clean architecture with separation of concerns
- Users API with full CRUD operations and validation
- PostgreSQL database with connection pooling
- JWT-based authentication with refresh tokens
- Redis caching for session management
- Structured logging with correlation IDs
- Environment-based configuration management
- Docker multi-stage builds for optimized images
- Health check endpoints for monitoring
- Graceful shutdown handling
Click to see solution
Project Structure:
user-api/
βββ cmd/
β βββ server/
β βββ main.go # Server setup and startup
βββ internal/
β βββ config/
β β βββ config.go # Configuration loading
β βββ handlers/
β β βββ auth.go # Authentication handlers
β β βββ users.go # User CRUD handlers
β β βββ middleware.go # Custom middleware
β βββ models/
β β βββ user.go # User model and validation
β βββ services/
β β βββ auth_service.go # Authentication business logic
β β βββ user_service.go # User management logic
β βββ database/
β β βββ postgres.go # Database connection
β βββ cache/
β βββ redis.go # Redis client
βββ pkg/
β βββ errors/ # Custom error types
β βββ logger/ # Logging utilities
βββ migrations/ # Database migrations
βββ docs/ # API documentation
βββ scripts/
β βββ build.sh # Build script
β βββ deploy.sh # Deployment script
βββ Dockerfile
βββ docker-compose.yml
βββ go.mod
βββ README.md
Configuration:
1package config
2
3type Config struct {
4 Server struct {
5 Host string `env:"SERVER_HOST" envDefault:"0.0.0.0"`
6 Port int `env:"SERVER_PORT" envDefault:"8080"`
7 ReadTimeout string `env:"SERVER_READ_TIMEOUT" envDefault:"30s"`
8 WriteTimeout string `env:"SERVER_WRITE_TIMEOUT" envDefault:"30s"`
9 } `json:"server"`
10 Database struct {
11 Host string `env:"DB_HOST" envDefault:"localhost"`
12 Port int `env:"DB_PORT" envDefault:"5432"`
13 Name string `env:"DB_NAME" envDefault:"userapi"`
14 User string `env:"DB_USER" envDefault:"postgres"`
15 Password string `env:"DB_PASSWORD"`
16 SSLMode string `env:"DB_SSL_MODE" envDefault:"disable"`
17 } `json:"database"`
18 Redis struct {
19 Host string `env:"REDIS_HOST" envDefault:"localhost"`
20 Port int `env:"REDIS_PORT" envDefault:"6379"`
21 Password string `env:"REDIS_PASSWORD"`
22 DB int `env:"REDIS_DB" envDefault:"0"`
23 } `json:"redis"`
24 JWT struct {
25 Secret string `env:"JWT_SECRET"`
26 Expiration string `env:"JWT_EXPIRATION" envDefault:"24h"`
27 } `json:"jwt"`
28 LogLevel string `env:"LOG_LEVEL" envDefault:"info"`
29}
Dockerfile:
1FROM golang:1.21-alpine AS builder
2
3WORKDIR /app
4COPY go.mod go.sum ./
5RUN go mod download
6
7COPY . .
8RUN CGO_ENABLED=0 GOOS=linux go build -o main cmd/server/main.go
9
10FROM alpine:latest
11RUN apk --no-cache add ca-certificates
12WORKDIR /root/
13
14COPY --from=builder /app/main .
15COPY --from=builder /app/migrations ./migrations
16
17EXPOSE 8080
18CMD ["./main"]
docker-compose.yml:
1version: '3.8'
2services:
3 api:
4 build: .
5 ports:
6 - "8080:8080"
7 environment:
8 - DB_HOST=postgres
9 - REDIS_HOST=redis
10 - JWT_SECRET=your-secret-key
11 depends_on:
12 - postgres
13 - redis
14
15 postgres:
16 image: postgres:15
17 environment:
18 - POSTGRES_DB=userapi
19 - POSTGRES_USER=postgres
20 - POSTGRES_PASSWORD=password
21 volumes:
22 - postgres_data:/var/lib/postgresql/data
23 ports:
24 - "5432:5432"
25
26 redis:
27 image: redis:7-alpine
28 ports:
29 - "6379:6379"
30 volumes:
31 - redis_data:/data
Exercise 3: Framework Migration Strategy
Learning Objective: Design a migration strategy to move from one framework to another without disrupting production services. Understand framework compatibility layers and gradual migration patterns.
Real-World Context: A company running Chi framework wants to migrate to Gin for better ecosystem support. The migration must happen gradually without downtime.
Difficulty: Advanced | Time Estimate: 60-90 minutes
Task: Create a migration strategy and implementation plan for moving from Chi to Gin framework.
Requirements:
- Zero-downtime migration approach
- Compatibility layer to run both frameworks simultaneously
- Traffic routing between old and new endpoints
- Rollback strategy for issues
- Comprehensive testing during migration
- Performance monitoring throughout migration
Click to see solution
Migration Strategy:
1// Step 1: Create compatibility layer
2type RouterWrapper struct {
3 chiRouter *chi.Mux
4 ginRouter *gin.Engine
5 migration *MigrationState
6}
7
8type MigrationState struct {
9 EnabledRoutes map[string]bool
10 TrafficSplit float64 // 0.0 = all Chi, 1.0 = all Gin
11}
12
13func NewRouterWrapper() *RouterWrapper {
14 return &RouterWrapper{
15 chiRouter: chi.NewRouter(),
16 ginRouter: gin.New(),
17 migration: &MigrationState{
18 EnabledRoutes: make(map[string]bool),
19 TrafficSplit: 0.0,
20 },
21 }
22}
23
24// Step 2: Implement gradual traffic shifting
25func (rw *RouterWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
26 route := r.Method + " " + r.URL.Path
27
28 // Check if route is migrated
29 if rw.migration.EnabledRoutes[route] {
30 // Use Gin for migrated routes
31 rw.ginRouter.ServeHTTP(w, r)
32 return
33 }
34
35 // Use traffic split for gradual rollout
36 if rand.Float64() < rw.migration.TrafficSplit {
37 rw.ginRouter.ServeHTTP(w, r)
38 } else {
39 rw.chiRouter.ServeHTTP(w, r)
40 }
41}
42
43// Step 3: Implement monitoring
44type MigrationMetrics struct {
45 ChiRequests int64
46 GinRequests int64
47 ChiErrors int64
48 GinErrors int64
49 ChiLatency time.Duration
50 GinLatency time.Duration
51}
52
53func (rw *RouterWrapper) monitoringMiddleware() gin.HandlerFunc {
54 return func(c *gin.Context) {
55 start := time.Now()
56 c.Next()
57
58 duration := time.Since(start)
59 statusCode := c.Writer.Status()
60
61 // Record metrics
62 metrics.RecordRequest("gin", duration, statusCode)
63 }
64}
65
66// Step 4: Enable feature flags
67func (rw *RouterWrapper) EnableRoute(route string) {
68 rw.migration.EnabledRoutes[route] = true
69}
70
71func (rw *RouterWrapper) SetTrafficSplit(split float64) {
72 rw.migration.TrafficSplit = split
73}
Migration Process:
- Deploy compatibility layer with 0% Gin traffic
- Migrate one route at a time, starting with low-traffic endpoints
- Monitor error rates and latency for each migrated route
- Gradually increase traffic split (10% β 25% β 50% β 100%)
- Disable Chi routes after successful migration
- Remove compatibility layer after full migration
Exercise 4: Enterprise Error Handling System
Learning Objective: Design structured error types that provide meaningful feedback to API consumers. Implement error handling middleware that works across different frameworks. Create consistent error response formats for better developer experience. Add proper error logging with context for debugging production issues.
Real-World Context: A public API serving 1M+ requests daily needs clear error messages for developers and detailed logging for operations teams. Poor error handling leads to support tickets and debugging difficulties.
Difficulty: Intermediate | Time Estimate: 45-60 minutes
Task: Implement a comprehensive error handling system that provides clear, actionable feedback to API consumers while enabling effective debugging for developers.
Core Requirements:
- Structured error responses with consistent JSON format
- Error types for common scenarios
- Proper HTTP status code mapping for different error categories
- Error logging with request context and correlation IDs
- Panic recovery middleware that prevents server crashes
- Internationalization support for error messages
Click to see solution
Error Types:
1package errors
2
3import (
4 "fmt"
5 "net/http"
6)
7
8// APIError represents a structured API error
9type APIError struct {
10 Code string `json:"code"`
11 Message string `json:"message"`
12 Details string `json:"details,omitempty"`
13 StatusCode int `json:"-"`
14 Cause error `json:"-"`
15 Context interface{} `json:"context,omitempty"`
16}
17
18func Error() string {
19 return fmt.Sprintf("%s: %s", e.Code, e.Message)
20}
21
22func Unwrap() error {
23 return e.Cause
24}
25
26// Predefined error types
27var (
28 ErrValidationFailed = APIError{
29 Code: "VALIDATION_FAILED",
30 Message: "Request validation failed",
31 StatusCode: http.StatusBadRequest,
32 }
33
34 ErrUnauthorized = APIError{
35 Code: "UNAUTHORIZED",
36 Message: "Authentication is required",
37 StatusCode: http.StatusUnauthorized,
38 }
39
40 ErrForbidden = APIError{
41 Code: "FORBIDDEN",
42 Message: "You don't have permission to access this resource",
43 StatusCode: http.StatusForbidden,
44 }
45
46 ErrNotFound = APIError{
47 Code: "NOT_FOUND",
48 Message: "The requested resource was not found",
49 StatusCode: http.StatusNotFound,
50 }
51
52 ErrConflict = APIError{
53 Code: "CONFLICT",
54 Message: "Resource conflict",
55 StatusCode: http.StatusConflict,
56 }
57
58 ErrInternalServer = APIError{
59 Code: "INTERNAL_SERVER_ERROR",
60 Message: "An internal server error occurred",
61 StatusCode: http.StatusInternalServerError,
62 }
63
64 ErrServiceUnavailable = APIError{
65 Code: "SERVICE_UNAVAILABLE",
66 Message: "Service temporarily unavailable",
67 StatusCode: http.StatusServiceUnavailable,
68 }
69)
70
71// Error constructors
72func NewValidationError(details string, cause error) APIError {
73 err := ErrValidationFailed
74 err.Details = details
75 err.Cause = cause
76 return err
77}
78
79func NewNotFoundError(resource string) APIError {
80 err := ErrNotFound
81 err.Details = fmt.Sprintf("%s not found", resource)
82 return err
83}
84
85func NewUnauthorizedError(details string) APIError {
86 err := ErrUnauthorized
87 err.Details = details
88 return err
89}
90
91func NewInternalError(cause error) APIError {
92 err := ErrInternalServer
93 err.Cause = cause
94 if cause != nil {
95 err.Details = cause.Error()
96 }
97 return err
98}
99
100func WithContext(err APIError, context interface{}) APIError {
101 err.Context = context
102 return err
103}
Error Handler:
1package handlers
2
3import (
4 "encoding/json"
5 "log/slog"
6 "net/http"
7 "time"
8
9 "your-project/pkg/errors"
10)
11
12type ErrorHandler struct {
13 logger *slog.Logger
14}
15
16func NewErrorHandler(logger *slog.Logger) *ErrorHandler {
17 return &ErrorHandler{logger: logger}
18}
19
20// Handle handles an APIError and writes response
21func Handle(w http.ResponseWriter, err error) {
22 var apiErr errors.APIError
23
24 // Type assert to APIError
25 if customErr, ok := err.(errors.APIError); ok {
26 apiErr = customErr
27 } else {
28 // Convert unknown errors to internal server error
29 apiErr = errors.NewInternalError(err)
30 }
31
32 // Log error
33 eh.logger.Error("API Error",
34 slog.String("code", apiErr.Code),
35 slog.String("message", apiErr.Message),
36 slog.String("details", apiErr.Details),
37 slog.Int("status_code", apiErr.StatusCode),
38 slog.Any("context", apiErr.Context),
39 slog.String("cause", apiErr.Cause.Error()),
40 )
41
42 // Write response
43 w.Header().Set("Content-Type", "application/json")
44 w.WriteHeader(apiErr.StatusCode)
45
46 response := map[string]interface{}{
47 "error": apiErr,
48 "timestamp": time.Now().UTC().Format(time.RFC3339),
49 }
50
51 json.NewEncoder(w).Encode(response)
52}
53
54// Recovery middleware
55func Recovery(next http.Handler) http.Handler {
56 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
57 defer func() {
58 if err := recover(); err != nil {
59 eh.logger.Error("Panic recovered",
60 slog.Any("panic", err),
61 slog.String("path", r.URL.Path),
62 slog.String("method", r.Method),
63 )
64
65 eh.Handle(w, errors.NewInternalError(
66 fmt.Errorf("panic: %v", err),
67 ))
68 }
69 }()
70
71 next.ServeHTTP(w, r)
72 })
73}
Usage Example:
1// In your handlers
2func GetUser(w http.ResponseWriter, r *http.Request) {
3 userID := r.URL.Query().Get("id")
4 if userID == "" {
5 h.errorHandler.Handle(w, errors.NewValidationError("user_id is required", nil))
6 return
7 }
8
9 user, err := h.userService.GetUser(userID)
10 if err != nil {
11 if errors.Is(err, sql.ErrNoRows) {
12 h.errorHandler.Handle(w, errors.NewNotFoundError("user"))
13 return
14 }
15 h.errorHandler.Handle(w, errors.NewInternalError(err))
16 return
17 }
18
19 // Return user...
20}
Exercise 5: Testing Framework Applications
Learning Objective: Write comprehensive tests for web framework applications including unit tests, integration tests, and end-to-end tests. Learn how to mock dependencies and test middleware chains.
Real-World Context: A payment processing API needs comprehensive test coverage before security audits. Tests must verify authentication, authorization, validation, and error handling.
Difficulty: Intermediate | Time Estimate: 75-90 minutes
Task: Create a complete test suite for a web framework application.
Requirements:
- Unit tests for individual handlers
- Integration tests for API endpoints
- Middleware testing with mocked dependencies
- Database interaction tests with test containers
- Error handling and edge case coverage
- Performance benchmarks for critical paths
Click to see solution
Testing Framework Applications:
1package handlers
2
3import (
4 "bytes"
5 "encoding/json"
6 "net/http"
7 "net/http/httptest"
8 "testing"
9
10 "github.com/gin-gonic/gin"
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/mock"
13)
14
15// Mock database
16type MockDatabase struct {
17 mock.Mock
18}
19
20func (m *MockDatabase) GetUser(id string) (*User, error) {
21 args := m.Called(id)
22 if args.Get(0) == nil {
23 return nil, args.Error(1)
24 }
25 return args.Get(0).(*User), args.Error(1)
26}
27
28func (m *MockDatabase) CreateUser(user *User) error {
29 args := m.Called(user)
30 return args.Error(0)
31}
32
33// Unit test for handler
34func TestGetUserHandler(t *testing.T) {
35 gin.SetMode(gin.TestMode)
36
37 tests := []struct {
38 name string
39 userID string
40 mockSetup func(*MockDatabase)
41 expectedStatus int
42 expectedBody map[string]interface{}
43 }{
44 {
45 name: "successful user retrieval",
46 userID: "123",
47 mockSetup: func(db *MockDatabase) {
48 db.On("GetUser", "123").Return(&User{
49 ID: "123",
50 Name: "John Doe",
51 Email: "john@example.com",
52 }, nil)
53 },
54 expectedStatus: http.StatusOK,
55 expectedBody: map[string]interface{}{
56 "id": "123",
57 "name": "John Doe",
58 "email": "john@example.com",
59 },
60 },
61 {
62 name: "user not found",
63 userID: "999",
64 mockSetup: func(db *MockDatabase) {
65 db.On("GetUser", "999").Return(nil, sql.ErrNoRows)
66 },
67 expectedStatus: http.StatusNotFound,
68 expectedBody: map[string]interface{}{
69 "error": "User not found",
70 },
71 },
72 }
73
74 for _, tt := range tests {
75 t.Run(tt.name, func(t *testing.T) {
76 // Setup
77 mockDB := new(MockDatabase)
78 tt.mockSetup(mockDB)
79
80 handler := &Handler{db: mockDB}
81 router := gin.New()
82 router.GET("/users/:id", handler.GetUser)
83
84 // Execute
85 w := httptest.NewRecorder()
86 req := httptest.NewRequest("GET", "/users/"+tt.userID, nil)
87 router.ServeHTTP(w, req)
88
89 // Assert
90 assert.Equal(t, tt.expectedStatus, w.Code)
91
92 var response map[string]interface{}
93 json.Unmarshal(w.Body.Bytes(), &response)
94
95 for key, expectedValue := range tt.expectedBody {
96 assert.Equal(t, expectedValue, response[key])
97 }
98
99 mockDB.AssertExpectations(t)
100 })
101 }
102}
103
104// Integration test
105func TestUserAPIIntegration(t *testing.T) {
106 // Setup test database
107 db := setupTestDatabase(t)
108 defer db.Close()
109
110 // Setup router
111 router := setupRouter(db)
112
113 t.Run("create and retrieve user", func(t *testing.T) {
114 // Create user
115 userData := map[string]interface{}{
116 "name": "Alice Smith",
117 "email": "alice@example.com",
118 "age": 28,
119 }
120 body, _ := json.Marshal(userData)
121
122 w := httptest.NewRecorder()
123 req := httptest.NewRequest("POST", "/api/users", bytes.NewReader(body))
124 req.Header.Set("Content-Type", "application/json")
125 router.ServeHTTP(w, req)
126
127 assert.Equal(t, http.StatusCreated, w.Code)
128
129 var createResponse map[string]interface{}
130 json.Unmarshal(w.Body.Bytes(), &createResponse)
131 userID := createResponse["id"].(string)
132
133 // Retrieve user
134 w = httptest.NewRecorder()
135 req = httptest.NewRequest("GET", "/api/users/"+userID, nil)
136 router.ServeHTTP(w, req)
137
138 assert.Equal(t, http.StatusOK, w.Code)
139
140 var getResponse map[string]interface{}
141 json.Unmarshal(w.Body.Bytes(), &getResponse)
142 assert.Equal(t, userData["name"], getResponse["name"])
143 assert.Equal(t, userData["email"], getResponse["email"])
144 })
145}
146
147// Middleware test
148func TestAuthMiddleware(t *testing.T) {
149 gin.SetMode(gin.TestMode)
150
151 tests := []struct {
152 name string
153 authHeader string
154 expectedStatus int
155 shouldCallNext bool
156 }{
157 {
158 name: "valid token",
159 authHeader: "Bearer valid-token",
160 expectedStatus: http.StatusOK,
161 shouldCallNext: true,
162 },
163 {
164 name: "missing token",
165 authHeader: "",
166 expectedStatus: http.StatusUnauthorized,
167 shouldCallNext: false,
168 },
169 {
170 name: "invalid token",
171 authHeader: "Bearer invalid-token",
172 expectedStatus: http.StatusUnauthorized,
173 shouldCallNext: false,
174 },
175 }
176
177 for _, tt := range tests {
178 t.Run(tt.name, func(t *testing.T) {
179 router := gin.New()
180
181 nextCalled := false
182 router.Use(authMiddleware())
183 router.GET("/protected", func(c *gin.Context) {
184 nextCalled = true
185 c.JSON(http.StatusOK, gin.H{"message": "success"})
186 })
187
188 w := httptest.NewRecorder()
189 req := httptest.NewRequest("GET", "/protected", nil)
190 if tt.authHeader != "" {
191 req.Header.Set("Authorization", tt.authHeader)
192 }
193 router.ServeHTTP(w, req)
194
195 assert.Equal(t, tt.expectedStatus, w.Code)
196 assert.Equal(t, tt.shouldCallNext, nextCalled)
197 })
198 }
199}
200
201// Performance benchmark
202func BenchmarkGetUser(b *testing.B) {
203 gin.SetMode(gin.TestMode)
204
205 mockDB := new(MockDatabase)
206 mockDB.On("GetUser", "123").Return(&User{
207 ID: "123",
208 Name: "John Doe",
209 Email: "john@example.com",
210 }, nil)
211
212 handler := &Handler{db: mockDB}
213 router := gin.New()
214 router.GET("/users/:id", handler.GetUser)
215
216 b.ResetTimer()
217 for i := 0; i < b.N; i++ {
218 w := httptest.NewRecorder()
219 req := httptest.NewRequest("GET", "/users/123", nil)
220 router.ServeHTTP(w, req)
221 }
222}
223
224// Helper function to setup test database
225func setupTestDatabase(t *testing.T) *sql.DB {
226 db, err := sql.Open("postgres", "postgres://localhost/test_db?sslmode=disable")
227 if err != nil {
228 t.Fatalf("Failed to open test database: %v", err)
229 }
230
231 // Run migrations
232 _, err = db.Exec(`
233 CREATE TABLE IF NOT EXISTS users (
234 id VARCHAR(255) PRIMARY KEY,
235 name VARCHAR(255) NOT NULL,
236 email VARCHAR(255) UNIQUE NOT NULL,
237 age INT NOT NULL,
238 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
239 )
240 `)
241 if err != nil {
242 t.Fatalf("Failed to create test schema: %v", err)
243 }
244
245 return db
246}
Testing Best Practices:
- Use table-driven tests for comprehensive coverage
- Mock external dependencies like databases and APIs
- Test both success and failure scenarios
- Verify middleware chains and execution order
- Benchmark critical paths for performance regressions
- Use test containers for integration tests with real databases
- Clean up test data after each test run
- Test concurrent request handling for race conditions
Testing Web Framework Applications
Web frameworks require comprehensive testing strategies that cover unit tests, integration tests, and end-to-end tests. Proper testing ensures reliability, catches regressions early, and provides confidence for production deployments.
Unit Testing Handlers
Unit tests focus on individual handler functions in isolation:
1// run
2package main
3
4import (
5 "encoding/json"
6 "net/http"
7 "net/http/httptest"
8 "testing"
9
10 "github.com/gin-gonic/gin"
11)
12
13func TestHealthCheckHandler(t *testing.T) {
14 // Setup
15 gin.SetMode(gin.TestMode)
16 router := gin.New()
17 router.GET("/health", healthCheckHandler)
18
19 // Execute
20 w := httptest.NewRecorder()
21 req := httptest.NewRequest("GET", "/health", nil)
22 router.ServeHTTP(w, req)
23
24 // Assert
25 if w.Code != http.StatusOK {
26 t.Errorf("Expected status 200, got %d", w.Code)
27 }
28
29 var response map[string]interface{}
30 json.Unmarshal(w.Body.Bytes(), &response)
31
32 if response["status"] != "healthy" {
33 t.Errorf("Expected status 'healthy', got %v", response["status"])
34 }
35}
36
37func healthCheckHandler(c *gin.Context) {
38 c.JSON(http.StatusOK, gin.H{
39 "status": "healthy",
40 "timestamp": "2024-01-01T00:00:00Z",
41 })
42}
Integration Testing
Integration tests verify that multiple components work together correctly:
1func TestUserAPIEndToEnd(t *testing.T) {
2 // Setup test database and router
3 db := setupTestDB(t)
4 defer db.Close()
5
6 router := setupRouter(db)
7
8 // Test create user
9 userData := `{"name":"Alice","email":"alice@example.com","age":25}`
10 w := httptest.NewRecorder()
11 req := httptest.NewRequest("POST", "/api/users", strings.NewReader(userData))
12 req.Header.Set("Content-Type", "application/json")
13 router.ServeHTTP(w, req)
14
15 if w.Code != http.StatusCreated {
16 t.Errorf("Expected status 201, got %d", w.Code)
17 }
18
19 var createResponse map[string]interface{}
20 json.Unmarshal(w.Body.Bytes(), &createResponse)
21 userID := createResponse["id"].(string)
22
23 // Test retrieve user
24 w = httptest.NewRecorder()
25 req = httptest.NewRequest("GET", "/api/users/"+userID, nil)
26 router.ServeHTTP(w, req)
27
28 if w.Code != http.StatusOK {
29 t.Errorf("Expected status 200, got %d", w.Code)
30 }
31}
Middleware Testing
Test middleware behavior and execution order:
1func TestAuthenticationMiddleware(t *testing.T) {
2 router := gin.New()
3 router.Use(authMiddleware())
4
5 protected := false
6 router.GET("/protected", func(c *gin.Context) {
7 protected = true
8 c.JSON(http.StatusOK, gin.H{"message": "success"})
9 })
10
11 // Test without auth
12 w := httptest.NewRecorder()
13 req := httptest.NewRequest("GET", "/protected", nil)
14 router.ServeHTTP(w, req)
15
16 if w.Code != http.StatusUnauthorized {
17 t.Errorf("Expected 401 without auth")
18 }
19 if protected {
20 t.Error("Handler should not be called without auth")
21 }
22
23 // Test with valid auth
24 w = httptest.NewRecorder()
25 req = httptest.NewRequest("GET", "/protected", nil)
26 req.Header.Set("Authorization", "Bearer valid-token")
27 router.ServeHTTP(w, req)
28
29 if w.Code != http.StatusOK {
30 t.Errorf("Expected 200 with valid auth")
31 }
32 if !protected {
33 t.Error("Handler should be called with valid auth")
34 }
35}
Deployment Strategies
Deploying web framework applications requires careful planning for reliability, scalability, and maintainability.
Docker Deployment
Containerization provides consistent environments across development and production:
1# Multi-stage build for Go application
2FROM golang:1.21-alpine AS builder
3
4WORKDIR /app
5
6# Copy dependencies first for better caching
7COPY go.mod go.sum ./
8RUN go mod download
9
10# Copy source code
11COPY . .
12
13# Build the application
14RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd/server
15
16# Production image
17FROM alpine:latest
18
19RUN apk --no-cache add ca-certificates
20
21WORKDIR /root/
22
23# Copy binary from builder
24COPY --from=builder /app/main .
25
26# Expose port
27EXPOSE 8080
28
29# Health check
30HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
31 CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
32
33# Run the application
34CMD ["./main"]
Kubernetes Deployment
For production-grade deployments with auto-scaling and high availability:
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: web-api
5 labels:
6 app: web-api
7spec:
8 replicas: 3
9 selector:
10 matchLabels:
11 app: web-api
12 template:
13 metadata:
14 labels:
15 app: web-api
16 spec:
17 containers:
18 - name: web-api
19 image: myorg/web-api:latest
20 ports:
21 - containerPort: 8080
22 env:
23 - name: PORT
24 value: "8080"
25 - name: DB_HOST
26 valueFrom:
27 secretKeyRef:
28 name: db-credentials
29 key: host
30 - name: DB_PASSWORD
31 valueFrom:
32 secretKeyRef:
33 name: db-credentials
34 key: password
35 resources:
36 requests:
37 memory: "128Mi"
38 cpu: "100m"
39 limits:
40 memory: "512Mi"
41 cpu: "500m"
42 livenessProbe:
43 httpGet:
44 path: /health
45 port: 8080
46 initialDelaySeconds: 10
47 periodSeconds: 30
48 readinessProbe:
49 httpGet:
50 path: /health
51 port: 8080
52 initialDelaySeconds: 5
53 periodSeconds: 10
54---
55apiVersion: v1
56kind: Service
57metadata:
58 name: web-api-service
59spec:
60 selector:
61 app: web-api
62 ports:
63 - protocol: TCP
64 port: 80
65 targetPort: 8080
66 type: LoadBalancer
67---
68apiVersion: autoscaling/v2
69kind: HorizontalPodAutoscaler
70metadata:
71 name: web-api-hpa
72spec:
73 scaleTargetRef:
74 apiVersion: apps/v1
75 kind: Deployment
76 name: web-api
77 minReplicas: 3
78 maxReplicas: 10
79 metrics:
80 - type: Resource
81 resource:
82 name: cpu
83 target:
84 type: Utilization
85 averageUtilization: 70
86 - type: Resource
87 resource:
88 name: memory
89 target:
90 type: Utilization
91 averageUtilization: 80
Graceful Shutdown
Implement graceful shutdown to handle in-flight requests during deployments:
1func main() {
2 router := setupRouter()
3
4 server := &http.Server{
5 Addr: ":8080",
6 Handler: router,
7 }
8
9 // Start server in goroutine
10 go func() {
11 if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
12 log.Fatalf("Server failed: %v", err)
13 }
14 }()
15
16 // Wait for interrupt signal
17 quit := make(chan os.Signal, 1)
18 signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
19 <-quit
20
21 log.Println("Shutting down server...")
22
23 // Create shutdown context with timeout
24 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
25 defer cancel()
26
27 // Attempt graceful shutdown
28 if err := server.Shutdown(ctx); err != nil {
29 log.Fatalf("Server forced to shutdown: %v", err)
30 }
31
32 log.Println("Server exited")
33}
Monitoring and Observability
Production applications require comprehensive monitoring to detect issues and understand system behavior.
Metrics Collection
Implement Prometheus metrics for monitoring:
1import (
2 "github.com/prometheus/client_golang/prometheus"
3 "github.com/prometheus/client_golang/prometheus/promauto"
4 "github.com/prometheus/client_golang/prometheus/promhttp"
5)
6
7var (
8 httpRequestsTotal = promauto.NewCounterVec(
9 prometheus.CounterOpts{
10 Name: "http_requests_total",
11 Help: "Total number of HTTP requests",
12 },
13 []string{"method", "endpoint", "status"},
14 )
15
16 httpRequestDuration = promauto.NewHistogramVec(
17 prometheus.HistogramOpts{
18 Name: "http_request_duration_seconds",
19 Help: "HTTP request latency in seconds",
20 Buckets: prometheus.DefBuckets,
21 },
22 []string{"method", "endpoint"},
23 )
24
25 activeConnections = promauto.NewGauge(
26 prometheus.GaugeOpts{
27 Name: "active_connections",
28 Help: "Number of active connections",
29 },
30 )
31)
32
33func metricsMiddleware() gin.HandlerFunc {
34 return func(c *gin.Context) {
35 start := time.Now()
36
37 activeConnections.Inc()
38 defer activeConnections.Dec()
39
40 c.Next()
41
42 duration := time.Since(start).Seconds()
43 status := strconv.Itoa(c.Writer.Status())
44
45 httpRequestsTotal.WithLabelValues(
46 c.Request.Method,
47 c.FullPath(),
48 status,
49 ).Inc()
50
51 httpRequestDuration.WithLabelValues(
52 c.Request.Method,
53 c.FullPath(),
54 ).Observe(duration)
55 }
56}
57
58func setupRouter() *gin.Engine {
59 r := gin.New()
60 r.Use(metricsMiddleware())
61
62 // Expose metrics endpoint
63 r.GET("/metrics", gin.WrapH(promhttp.Handler()))
64
65 return r
66}
Structured Logging
Use structured logging for better log analysis:
1import (
2 "log/slog"
3 "os"
4)
5
6func setupLogger() *slog.Logger {
7 handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
8 Level: slog.LevelInfo,
9 AddSource: true,
10 })
11
12 return slog.New(handler)
13}
14
15func loggingMiddleware(logger *slog.Logger) gin.HandlerFunc {
16 return func(c *gin.Context) {
17 start := time.Now()
18 path := c.Request.URL.Path
19 method := c.Request.Method
20
21 c.Next()
22
23 logger.Info("HTTP request",
24 slog.String("method", method),
25 slog.String("path", path),
26 slog.Int("status", c.Writer.Status()),
27 slog.Duration("latency", time.Since(start)),
28 slog.String("ip", c.ClientIP()),
29 slog.String("user_agent", c.Request.UserAgent()),
30 )
31 }
32}
Distributed Tracing
Implement distributed tracing for microservices:
1import (
2 "go.opentelemetry.io/otel"
3 "go.opentelemetry.io/otel/trace"
4)
5
6func tracingMiddleware() gin.HandlerFunc {
7 tracer := otel.Tracer("web-api")
8
9 return func(c *gin.Context) {
10 ctx, span := tracer.Start(c.Request.Context(), c.FullPath())
11 defer span.End()
12
13 // Add attributes to span
14 span.SetAttributes(
15 attribute.String("http.method", c.Request.Method),
16 attribute.String("http.url", c.Request.URL.String()),
17 attribute.String("http.user_agent", c.Request.UserAgent()),
18 )
19
20 // Store context in gin context
21 c.Request = c.Request.WithContext(ctx)
22
23 c.Next()
24
25 // Record response status
26 span.SetAttributes(
27 attribute.Int("http.status_code", c.Writer.Status()),
28 )
29 }
30}
Further Reading
- Gin & Echo Frameworks - Complete Implementation Guide
- Fiber & Chi Frameworks - Complete Implementation Guide
- Go Standard Library HTTP Server
- REST API Design Patterns
- Middleware and Request Processing
Summary
Key Takeaways
π― Framework Selection Criteria:
- Performance vs. Compatibility: Consider your specific requirements - raw performance vs ecosystem compatibility
- Team Experience: Match framework choice to your team's background and learning goals
- Long-term Vision: Consider maintenance, hiring, and growth trajectories
- Ecosystem Requirements: Evaluate available middleware, monitoring tools, and third-party integrations
- Migration Flexibility: Plan for potential framework changes and technology evolution
Framework Recommendations
Choose Chi when:
- β Long-term maintainability is more important than development speed
- β You need to integrate with existing net/http infrastructure
- β Enterprise environments with strict compliance requirements
- β You value ecosystem compatibility and standard Go patterns
- β You want to gradually migrate from standard library
Choose Gin when:
- β You need a balance of performance and ecosystem support
- β Your team has Go experience and wants good productivity
- β You're building REST APIs with standard requirements
- β You prefer Express.js-style APIs with better Go integration
Choose Echo when:
- β You need more flexible middleware composition than Gin
- β Built-in TLS and validation are important for your project
- β You prefer cleaner, more minimalist API design
- β Your team values simplicity and extensibility
Choose Fiber when:
- β Performance is absolutely critical
- β Your requirements are simple and self-contained
- β Your team comes from Node.js/Express.js background
- β You're building microservices that don't need complex integrations
Production Best Practices
β οΈ Critical Success Factors:
- Start with standard patterns: Use established Go patterns and idioms
- Implement structured logging: Use correlation IDs for request tracing
- Add comprehensive error handling: Create consistent error responses and recovery mechanisms
- Security first approach: Implement authentication, authorization, and security headers
- Plan for scalability: Use connection pooling, caching, and horizontal scaling
- Monitor and observe: Add metrics, tracing, and health checks
- Use environment-based configuration: Separate config from code and validate at startup
- Write comprehensive tests: Unit tests for business logic, integration tests for APIs
When to Consider Each Framework
High-Performance APIs: Fiber β Gin β Echo β Chi
Enterprise Applications: Chi β Echo β Gin β Fiber
Team with Node.js Experience: Fiber β Gin β Echo β Chi
Quick Prototyping: Fiber β Gin β Echo β Chi
Maximum Ecosystem Compatibility: Chi β Echo β Gin β Fiber