Advanced Web Frameworks in Go: Gin and Echo
/ 9 min read
Advanced Web Frameworks in Go: Gin and Echo
This guide demonstrates how to build high-performance web applications using two popular Go web frameworks: Gin and Echo. We’ll create the same REST API using both frameworks to compare their features and approaches.
Example Project: Task Management API
We’ll build a task management API with the following features:
- CRUD operations for tasks
- User authentication
- Request validation
- Custom middleware
- File upload/download
- Error handling
Part 1: Using Gin Framework
Project Structure (Gin)
task-api-gin/├── handlers/│ └── task.go├── middleware/│ └── auth.go├── models/│ └── task.go├── main.go└── go.mod1. Models
package models
import "time"
type Task struct { ID uint `json:"id"` Title string `json:"title" binding:"required,min=3"` Description string `json:"description"` Status string `json:"status" binding:"required,oneof=pending in_progress completed"` DueDate time.Time `json:"due_date" binding:"required,gtefield=time.Now"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` UserID uint `json:"user_id"`}
type TaskFilter struct { Status string `form:"status"` Search string `form:"search"`}2. Middleware
package middleware
import ( "net/http" "strings"
"github.com/gin-gonic/gin")
func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"}) c.Abort() return }
// Simple token validation (in production, use proper JWT validation) token := strings.TrimPrefix(authHeader, "Bearer ") if token == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token format"}) c.Abort() return }
// Set user ID (in production, extract from JWT) c.Set("user_id", uint(1)) c.Next() }}3. Handlers
package handlers
import ( "net/http" "strconv" "time"
"github.com/gin-gonic/gin" "task-api-gin/models")
type TaskHandler struct { tasks []models.Task // In production, use a database}
func NewTaskHandler() *TaskHandler { return &TaskHandler{ tasks: make([]models.Task, 0), }}
// Create a new taskfunc (h *TaskHandler) CreateTask(c *gin.Context) { var task models.Task if err := c.ShouldBindJSON(&task); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return }
// Set metadata task.ID = uint(len(h.tasks) + 1) task.CreatedAt = time.Now() task.UpdatedAt = time.Now() task.UserID = c.GetUint("user_id")
h.tasks = append(h.tasks, task)
c.JSON(http.StatusCreated, task)}
// Get all tasks with filteringfunc (h *TaskHandler) ListTasks(c *gin.Context) { var filter models.TaskFilter if err := c.ShouldBindQuery(&filter); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return }
userID := c.GetUint("user_id") var filteredTasks []models.Task
for _, task := range h.tasks { if task.UserID != userID { continue }
if filter.Status != "" && task.Status != filter.Status { continue }
if filter.Search != "" && !strings.Contains( strings.ToLower(task.Title), strings.ToLower(filter.Search), ) { continue }
filteredTasks = append(filteredTasks, task) }
c.JSON(http.StatusOK, filteredTasks)}
// Get task by IDfunc (h *TaskHandler) GetTask(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"}) return }
userID := c.GetUint("user_id")
for _, task := range h.tasks { if task.ID == uint(id) && task.UserID == userID { c.JSON(http.StatusOK, task) return } }
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})}
// Update taskfunc (h *TaskHandler) UpdateTask(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"}) return }
var updatedTask models.Task if err := c.ShouldBindJSON(&updatedTask); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return }
userID := c.GetUint("user_id")
for i, task := range h.tasks { if task.ID == uint(id) && task.UserID == userID { updatedTask.ID = task.ID updatedTask.CreatedAt = task.CreatedAt updatedTask.UpdatedAt = time.Now() updatedTask.UserID = userID
h.tasks[i] = updatedTask c.JSON(http.StatusOK, updatedTask) return } }
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})}
// Delete taskfunc (h *TaskHandler) DeleteTask(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"}) return }
userID := c.GetUint("user_id")
for i, task := range h.tasks { if task.ID == uint(id) && task.UserID == userID { h.tasks = append(h.tasks[:i], h.tasks[i+1:]...) c.Status(http.StatusNoContent) return } }
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})}
// Upload task attachmentfunc (h *TaskHandler) UploadAttachment(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"}) return }
file, err := c.FormFile("file") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "No file uploaded"}) return }
// Save file (in production, use proper storage service) filename := fmt.Sprintf("task_%d_%s", id, file.Filename) if err := c.SaveUploadedFile(file, filename); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) return }
c.JSON(http.StatusOK, gin.H{ "message": "File uploaded successfully", "filename": filename, })}4. Main Application
package main
import ( "log"
"github.com/gin-gonic/gin" "task-api-gin/handlers" "task-api-gin/middleware")
func main() { r := gin.Default()
// Add global middleware r.Use(gin.Recovery()) r.Use(gin.Logger())
// Create handler taskHandler := handlers.NewTaskHandler()
// Public routes r.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"status": "healthy"}) })
// Protected routes api := r.Group("/api") api.Use(middleware.AuthMiddleware()) { // Task routes tasks := api.Group("/tasks") { tasks.POST("", taskHandler.CreateTask) tasks.GET("", taskHandler.ListTasks) tasks.GET("/:id", taskHandler.GetTask) tasks.PUT("/:id", taskHandler.UpdateTask) tasks.DELETE("/:id", taskHandler.DeleteTask) tasks.POST("/:id/attachment", taskHandler.UploadAttachment) } }
// Start server if err := r.Run(":8080"); err != nil { log.Fatal(err) }}Part 2: Using Echo Framework
Project Structure (Echo)
task-api-echo/├── handlers/│ └── task.go├── middleware/│ └── auth.go├── models/│ └── task.go├── main.go└── go.mod1. Models
package models
import "time"
type Task struct { ID uint `json:"id"` Title string `json:"title" validate:"required,min=3"` Description string `json:"description"` Status string `json:"status" validate:"required,oneof=pending in_progress completed"` DueDate time.Time `json:"due_date" validate:"required,gtefield=time.Now"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` UserID uint `json:"user_id"`}
type TaskFilter struct { Status string `query:"status"` Search string `query:"search"`}2. Middleware
package middleware
import ( "strings"
"github.com/labstack/echo/v4")
func AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { authHeader := c.Request().Header.Get("Authorization") if authHeader == "" { return echo.NewHTTPError(401, "Authorization header required") }
// Simple token validation (in production, use proper JWT validation) token := strings.TrimPrefix(authHeader, "Bearer ") if token == "" { return echo.NewHTTPError(401, "Invalid token format") }
// Set user ID (in production, extract from JWT) c.Set("user_id", uint(1)) return next(c) }}3. Handlers
package handlers
import ( "net/http" "strconv" "time"
"github.com/labstack/echo/v4" "task-api-echo/models")
type TaskHandler struct { tasks []models.Task // In production, use a database}
func NewTaskHandler() *TaskHandler { return &TaskHandler{ tasks: make([]models.Task, 0), }}
// Create a new taskfunc (h *TaskHandler) CreateTask(c echo.Context) error { var task models.Task if err := c.Bind(&task); err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) }
if err := c.Validate(&task); err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) }
// Set metadata task.ID = uint(len(h.tasks) + 1) task.CreatedAt = time.Now() task.UpdatedAt = time.Now() task.UserID = c.Get("user_id").(uint)
h.tasks = append(h.tasks, task)
return c.JSON(http.StatusCreated, task)}
// Get all tasks with filteringfunc (h *TaskHandler) ListTasks(c echo.Context) error { var filter models.TaskFilter if err := c.Bind(&filter); err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) }
userID := c.Get("user_id").(uint) var filteredTasks []models.Task
for _, task := range h.tasks { if task.UserID != userID { continue }
if filter.Status != "" && task.Status != filter.Status { continue }
if filter.Search != "" && !strings.Contains( strings.ToLower(task.Title), strings.ToLower(filter.Search), ) { continue }
filteredTasks = append(filteredTasks, task) }
return c.JSON(http.StatusOK, filteredTasks)}
// Get task by IDfunc (h *TaskHandler) GetTask(c echo.Context) error { id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Invalid task ID") }
userID := c.Get("user_id").(uint)
for _, task := range h.tasks { if task.ID == uint(id) && task.UserID == userID { return c.JSON(http.StatusOK, task) } }
return echo.NewHTTPError(http.StatusNotFound, "Task not found")}
// Update taskfunc (h *TaskHandler) UpdateTask(c echo.Context) error { id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Invalid task ID") }
var updatedTask models.Task if err := c.Bind(&updatedTask); err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) }
if err := c.Validate(&updatedTask); err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) }
userID := c.Get("user_id").(uint)
for i, task := range h.tasks { if task.ID == uint(id) && task.UserID == userID { updatedTask.ID = task.ID updatedTask.CreatedAt = task.CreatedAt updatedTask.UpdatedAt = time.Now() updatedTask.UserID = userID
h.tasks[i] = updatedTask return c.JSON(http.StatusOK, updatedTask) } }
return echo.NewHTTPError(http.StatusNotFound, "Task not found")}
// Delete taskfunc (h *TaskHandler) DeleteTask(c echo.Context) error { id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Invalid task ID") }
userID := c.Get("user_id").(uint)
for i, task := range h.tasks { if task.ID == uint(id) && task.UserID == userID { h.tasks = append(h.tasks[:i], h.tasks[i+1:]...) return c.NoContent(http.StatusNoContent) } }
return echo.NewHTTPError(http.StatusNotFound, "Task not found")}
// Upload task attachmentfunc (h *TaskHandler) UploadAttachment(c echo.Context) error { id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Invalid task ID") }
file, err := c.FormFile("file") if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "No file uploaded") }
// Save file (in production, use proper storage service) filename := fmt.Sprintf("task_%d_%s", id, file.Filename) if err := c.SaveUploadedFile(file, filename); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to save file") }
return c.JSON(http.StatusOK, map[string]string{ "message": "File uploaded successfully", "filename": filename, })}4. Main Application
package main
import ( "log"
"github.com/go-playground/validator/v10" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "task-api-echo/handlers" customMiddleware "task-api-echo/middleware")
type CustomValidator struct { validator *validator.Validate}
func (cv *CustomValidator) Validate(i interface{}) error { return cv.validator.Struct(i)}
func main() { e := echo.New()
// Set custom validator e.Validator = &CustomValidator{validator: validator.New()}
// Add global middleware e.Use(middleware.Recover()) e.Use(middleware.Logger())
// Create handler taskHandler := handlers.NewTaskHandler()
// Public routes e.GET("/health", func(c echo.Context) error { return c.JSON(200, map[string]string{"status": "healthy"}) })
// Protected routes api := e.Group("/api", customMiddleware.AuthMiddleware) { // Task routes tasks := api.Group("/tasks") tasks.POST("", taskHandler.CreateTask) tasks.GET("", taskHandler.ListTasks) tasks.GET("/:id", taskHandler.GetTask) tasks.PUT("/:id", taskHandler.UpdateTask) tasks.DELETE("/:id", taskHandler.DeleteTask) tasks.POST("/:id/attachment", taskHandler.UploadAttachment) }
// Start server if err := e.Start(":8080"); err != nil { log.Fatal(err) }}Comparison: Gin vs Echo
1. Request Handling
- Gin: Uses
gin.Contextwith built-in binding and validation - Echo: Uses
echo.Contextwith separate validator implementation
2. Middleware
- Gin: Middleware uses
gin.HandlerFunc - Echo: Middleware uses
echo.MiddlewareFunc
3. Error Handling
- Gin: Uses
c.JSON()for error responses - Echo: Uses
echo.NewHTTPError()for structured errors
4. Route Groups
- Gin: Uses
r.Group()with closure syntax - Echo: Uses
e.Group()with method chaining
5. Performance
Both frameworks are highly performant, with similar benchmarks. Choose based on:
- Gin: If you want more built-in features
- Echo: If you prefer a more minimalist approach
6. Community and Ecosystem
- Gin: Larger community, more third-party packages
- Echo: Smaller but active community, cleaner API design
Best Practices
-
Error Handling
- Use custom error types
- Implement proper error middleware
- Return consistent error responses
-
Validation
- Use struct tags for validation
- Implement custom validators when needed
- Validate at the handler level
-
Middleware
- Keep middleware focused and reusable
- Use middleware for cross-cutting concerns
- Chain middleware appropriately
-
Security
- Implement proper authentication
- Use HTTPS in production
- Validate and sanitize input
-
Testing
- Write unit tests for handlers
- Use table-driven tests
- Mock external dependencies
-
Documentation
- Use OpenAPI/Swagger
- Document all endpoints
- Include example requests/responses
Production Considerations
-
Configuration
- Use environment variables
- Implement proper config management
- Use different configs per environment
-
Logging
- Implement structured logging
- Use proper log levels
- Consider log aggregation
-
Monitoring
- Add metrics collection
- Implement health checks
- Use proper APM tools
-
Database
- Use connection pooling
- Implement proper migrations
- Handle database errors gracefully
-
Caching
- Implement response caching
- Use Redis for distributed caching
- Cache at appropriate levels
-
Deployment
- Use Docker containers
- Implement proper CI/CD
- Use load balancing
This example demonstrates how to build a production-ready REST API using both Gin and Echo frameworks. Both implementations show best practices for structure, error handling, validation, and security.