Gin vs. Echo: Choosing a Go Web Framework
Introduction
Gin and Echo are Go's two most popular HTTP frameworks. Both are fast, lightweight, and actively maintained — but they make different design choices that matter in practice. This post compares them with real code so you can make an informed decision.
Quick Summary
| Gin | Echo | |
|---|---|---|
| GitHub stars | ~78k | ~29k |
| Router | httprouter fork | radix tree |
| Middleware style | gin.HandlerFunc | echo.MiddlewareFunc |
| Context | *gin.Context | echo.Context (interface) |
| Template rendering | Basic, bring your own | Built-in, multiple engines |
| Validator | go-playground/validator | Built-in via interface |
| Learning curve | Gentle | Moderate |
| Best for | APIs, microservices | APIs + server-rendered apps |
Routing
Both frameworks support path parameters, wildcards, and route grouping, but the syntax differs slightly.
Gin:
r := gin.Default()
// Simple route
r.GET("/users/:id", getUser)
// Route group with shared prefix
api := r.Group("/api/v1")
{
api.GET("/users", listUsers)
api.POST("/users", createUser)
api.PUT("/users/:id", updateUser)
api.DELETE("/users/:id", deleteUser)
}
r.Run(":8080")
Echo:
e := echo.New()
// Simple route
e.GET("/users/:id", getUser)
// Route group with shared prefix
api := e.Group("/api/v1")
api.GET("/users", listUsers)
api.POST("/users", createUser)
api.PUT("/users/:id", updateUser)
api.DELETE("/users/:id", deleteUser)
e.Start(":8080")
Echo's route groups don't require the extra {} block — a minor but consistent difference.
Handler and Context
The context object is where the two frameworks diverge most.
Gin handler:
func getUser(c *gin.Context) {
id := c.Param("id")
page := c.DefaultQuery("page", "1")
user, err := db.FindUser(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
c.JSON(http.StatusOK, user)
}
Echo handler:
func getUser(c echo.Context) error {
id := c.Param("id")
page := c.QueryParam("page")
if page == "" {
page = "1"
}
user, err := db.FindUser(id)
if err != nil {
return c.JSON(http.StatusNotFound, map[string]string{"error": "user not found"})
}
return c.JSON(http.StatusOK, user)
}
Key difference: Echo handlers return error. This enables clean error propagation — you can return err and let a global error handler format the response. Gin handlers have no return value; you call c.JSON() and then return.
Echo's approach leads to cleaner error handling in complex handlers:
// Echo — just return the error, central handler formats it
func createOrder(c echo.Context) error {
var req OrderRequest
if err := c.Bind(&req); err != nil {
return err // central handler returns 400
}
if err := c.Validate(&req); err != nil {
return err // central handler returns 422
}
order, err := db.CreateOrder(req)
if err != nil {
return err // central handler returns 500
}
return c.JSON(http.StatusCreated, order)
}
Middleware
Both frameworks use chainable middleware, but the signatures differ.
Gin middleware:
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if !isValid(token) {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
c.Set("userID", extractUserID(token))
c.Next() // call the next handler
}
}
// Apply to a group
protected := r.Group("/admin")
protected.Use(AuthMiddleware())
Echo middleware:
func AuthMiddleware() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token := c.Request().Header.Get("Authorization")
if !isValid(token) {
return echo.ErrUnauthorized
}
c.Set("userID", extractUserID(token))
return next(c) // call the next handler
}
}
}
// Apply to a group
admin := e.Group("/admin", AuthMiddleware())
Echo's middleware follows the classic next-based chain pattern, familiar from Node.js/Koa. Gin's c.Next() / c.Abort() model is less common but intuitive once learned.
Built-in Middleware
Both ship with common middleware out of the box:
| Middleware | Gin | Echo |
|---|---|---|
| Logger | gin.Logger() | middleware.Logger() |
| Recovery | gin.Recovery() | middleware.Recover() |
| CORS | external package | middleware.CORS() |
| Rate limiting | external package | middleware.RateLimiter() |
| GZIP | external package | middleware.Gzip() |
| JWT | external package | middleware.JWT() |
Echo ships with more middleware out of the box. Gin relies on the community (gin-contrib) for extras.
Request Binding and Validation
Gin uses go-playground/validator for struct validation:
type CreateUserRequest struct {
Name string `json:"name" binding:"required,min=2"`
Email string `json:"email" binding:"required,email"`
}
func createUser(c *gin.Context) {
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// req is now validated
}
Echo uses a pluggable validator interface — you wire in your own:
type CustomValidator struct {
validator *validator.Validate
}
func (cv *CustomValidator) Validate(i interface{}) error {
return cv.validator.Struct(i)
}
e.Validator = &CustomValidator{validator: validator.New()}
func createUser(c echo.Context) error {
var req CreateUserRequest
if err := c.Bind(&req); err != nil {
return err
}
if err := c.Validate(&req); err != nil {
return err
}
return c.JSON(http.StatusCreated, req)
}
Gin's binding is simpler to get started. Echo's pluggable validator gives more control.
Performance
Both are within ~5–10% of each other in benchmarks and far faster than standard net/http with a mux. In practice, your database and business logic will dominate — the framework overhead is negligible.
For reference, both handle hundreds of thousands of requests per second on commodity hardware for simple JSON API routes.
Which Should You Choose?
Choose Gin if:
- You want the most popular framework with the largest ecosystem
- You prefer a simpler getting-started experience
- Your project is a pure REST API or microservice
Choose Echo if:
- You want clean error handling via return values
- You need built-in middleware (CORS, JWT, rate limiting) without extra packages
- You are building both an API and server-rendered HTML templates
- You want a pluggable validator interface
Both are production-grade. Many teams pick Gin simply because of familiarity; Echo tends to win on code cleanliness for larger projects.
