skip to content
Astro Cactus

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:

  1. CRUD operations for tasks
  2. User authentication
  3. Request validation
  4. Custom middleware
  5. File upload/download
  6. Error handling

Part 1: Using Gin Framework

Project Structure (Gin)

Terminal window
task-api-gin/
├── handlers/
└── task.go
├── middleware/
└── auth.go
├── models/
└── task.go
├── main.go
└── go.mod

1. Models

models/task.go
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

middleware/auth.go
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

handlers/task.go
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 task
func (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 filtering
func (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 ID
func (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 task
func (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 task
func (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 attachment
func (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

main.go
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)

Terminal window
task-api-echo/
├── handlers/
└── task.go
├── middleware/
└── auth.go
├── models/
└── task.go
├── main.go
└── go.mod

1. Models

models/task.go
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

middleware/auth.go
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

handlers/task.go
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 task
func (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 filtering
func (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 ID
func (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 task
func (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 task
func (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 attachment
func (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

main.go
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.Context with built-in binding and validation
  • Echo: Uses echo.Context with 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

  1. Error Handling

    • Use custom error types
    • Implement proper error middleware
    • Return consistent error responses
  2. Validation

    • Use struct tags for validation
    • Implement custom validators when needed
    • Validate at the handler level
  3. Middleware

    • Keep middleware focused and reusable
    • Use middleware for cross-cutting concerns
    • Chain middleware appropriately
  4. Security

    • Implement proper authentication
    • Use HTTPS in production
    • Validate and sanitize input
  5. Testing

    • Write unit tests for handlers
    • Use table-driven tests
    • Mock external dependencies
  6. Documentation

    • Use OpenAPI/Swagger
    • Document all endpoints
    • Include example requests/responses

Production Considerations

  1. Configuration

    • Use environment variables
    • Implement proper config management
    • Use different configs per environment
  2. Logging

    • Implement structured logging
    • Use proper log levels
    • Consider log aggregation
  3. Monitoring

    • Add metrics collection
    • Implement health checks
    • Use proper APM tools
  4. Database

    • Use connection pooling
    • Implement proper migrations
    • Handle database errors gracefully
  5. Caching

    • Implement response caching
    • Use Redis for distributed caching
    • Cache at appropriate levels
  6. 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.