Gin vs Echo vs Fiber vs Chi: Go Web Frameworks 2026
Go web framework comparison 2026: Gin (most popular), Echo (performance+middleware), Fiber (Express-inspired, fastest), Chi (stdlib-compatible), benchmarks, and when to choose each.
Go has a rich web framework ecosystem, but each framework makes different tradeoffs. Gin prioritizes ecosystem and stability. Echo offers clean ergonomics. Fiber chases maximum throughput with an Express-like API. Chi stays close to the standard library. In 2026, all four are production-ready โ the choice comes down to your teamโs priorities.
Framework Snapshot
| Framework | Stars | Req/sec (TechEmpower) | Go version | net/http compatible |
|---|---|---|---|---|
| Gin | 80K | 250,000 | 1.21+ | Yes |
| Echo | 30K | 230,000 | 1.21+ | Yes |
| Fiber | 34K | 290,000 | 1.21+ | No (fasthttp) |
| Chi | 18K | 220,000 | 1.21+ | Yes |
The โnet/http compatibleโ column matters: Fiber uses fasthttp under the hood, which is incompatible with standard http.Handler middleware.
Hello World Comparison
Gin
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.Run(":8080")
}
Echo
package main
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.GET("/ping", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"message": "pong"})
})
e.Logger.Fatal(e.Start(":8080"))
}
Fiber
package main
import "github.com/gofiber/fiber/v2"
func main() {
app := fiber.New()
app.Get("/ping", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"message": "pong"})
})
app.Listen(":8080")
}
Chi
package main
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"message": "pong"})
})
http.ListenAndServe(":8080", r)
}
Routing and URL Parameters
Gin
r := gin.Default()
// Path parameters
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"id": id})
})
// Query parameters
r.GET("/users", func(c *gin.Context) {
page := c.DefaultQuery("page", "1")
search := c.Query("search")
c.JSON(200, gin.H{"page": page, "search": search})
})
// Route groups
v1 := r.Group("/api/v1")
{
v1.GET("/users", listUsers)
v1.POST("/users", createUser)
v1.GET("/users/:id", getUser)
v1.PUT("/users/:id", updateUser)
v1.DELETE("/users/:id", deleteUser)
}
Echo
e := echo.New()
e.GET("/users/:id", func(c echo.Context) error {
id := c.Param("id")
return c.JSON(200, map[string]string{"id": id})
})
// Route groups with middleware
api := e.Group("/api", middleware.JWT([]byte("secret")))
api.GET("/users", listUsers)
api.POST("/users", createUser)
Fiber
app := fiber.New()
app.Get("/users/:id", func(c *fiber.Ctx) error {
id := c.Params("id")
return c.JSON(fiber.Map{"id": id})
})
// Named params with optional
app.Get("/users/:id?", handler)
// Wildcard
app.Get("/static/*", handler)
Chi
r := chi.NewRouter()
r.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
json.NewEncoder(w).Encode(map[string]string{"id": id})
})
// Inline route groups
r.Route("/api/v1", func(r chi.Router) {
r.Use(authMiddleware)
r.Get("/users", listUsers)
r.Post("/users", createUser)
r.Route("/users/{id}", func(r chi.Router) {
r.Get("/", getUser)
r.Put("/", updateUser)
r.Delete("/", deleteUser)
})
})
Middleware Comparison
Gin Middleware
// Built-in
r := gin.New()
r.Use(gin.Logger()) // request logging
r.Use(gin.Recovery()) // panic recovery
// Custom middleware
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(401, gin.H{"error": "unauthorized"})
c.Abort()
return
}
c.Set("user_id", validateToken(token))
c.Next()
}
}
// Apply to group
protected := r.Group("/admin")
protected.Use(AuthMiddleware())
Echo Middleware
import "github.com/labstack/echo/v4/middleware"
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{Level: 5}))
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20)))
// Custom
func authMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token := c.Request().Header.Get("Authorization")
if token == "" {
return echo.ErrUnauthorized
}
c.Set("user_id", validateToken(token))
return next(c)
}
}
Fiber Middleware
import (
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/fiber/v2/middleware/limiter"
)
app.Use(logger.New())
app.Use(recover.New())
app.Use(cors.New(cors.Config{
AllowOrigins: "https://example.com",
AllowHeaders: "Origin, Content-Type, Authorization",
}))
app.Use(limiter.New(limiter.Config{
Max: 20,
Expiration: 30 * time.Second,
}))
JSON Binding and Validation
Gin โ struct binding
type CreateUserInput struct {
Name string `json:"name" binding:"required,min=2,max=50"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"required,min=18,max=120"`
}
func createUser(c *gin.Context) {
var input CreateUserInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(422, gin.H{"error": err.Error()})
return
}
// input is validated
c.JSON(201, gin.H{"name": input.Name})
}
Echo โ struct binding
type CreateUserInput struct {
Name string `json:"name" validate:"required,min=2,max=50"`
Email string `json:"email" validate:"required,email"`
}
// Register validator
import "github.com/go-playground/validator/v10"
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 input CreateUserInput
if err := c.Bind(&input); err != nil {
return echo.NewHTTPError(400, err.Error())
}
if err := c.Validate(input); err != nil {
return echo.NewHTTPError(422, err.Error())
}
return c.JSON(201, input)
}
Benchmark Results (TechEmpower Round 22)
Plaintext (req/sec, higher is better):
| Framework | Single query | Multiple queries | Data updates |
|---|---|---|---|
| Fiber | 290,000 | 145,000 | 82,000 |
| Gin | 250,000 | 128,000 | 74,000 |
| Echo | 235,000 | 122,000 | 70,000 |
| Chi | 218,000 | 115,000 | 68,000 |
Memory usage at 10k concurrent connections:
| Framework | Memory |
|---|---|
| Fiber | 48MB |
| Chi | 52MB |
| Gin | 68MB |
| Echo | 72MB |
Fiberโs fasthttp foundation gives it a consistent throughput edge, but the gap narrows with real database workloads.
When to Choose Each
Choose Gin when:
- Maximum ecosystem maturity (most middleware, most tutorials)
- Team already knows Gin
- Need
net/httpmiddleware compatibility - Building APIs where standard approach > cutting edge
- Large team โ Ginโs patterns are well-documented
Choose Echo when:
- Clean, readable API design matters
- Need automatic TLS with Letโs Encrypt (
e.StartAutoTLS(":443")) - Built-in middleware coverage (CORS, rate limiting, JWT) is a priority
- Building REST APIs with good ergonomics
Choose Fiber when:
- Absolute maximum throughput is required
- Coming from Express.js (very similar API)
- Building high-performance services (proxy, API gateway)
- Okay with fasthttpโs incompatibility with net/http middleware
Choose Chi when:
- Value stdlib compatibility above all
- Want to use any
http.Handlermiddleware - Building services that may need to swap frameworks later
- Minimal abstractions, pure Go idioms
Final Recommendation
For most new Go web services in 2026, Gin remains the safe default โ the largest ecosystem, most documentation, and solid performance. Fiber is the right choice when youโre benchmarking and need every request/second. Chi is excellent for teams that want the lightest abstraction on top of the standard library. Echo is a polished middle ground with the cleanest API design.
50 CLI Commands That Save Hours Every Week
The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.
- Git mastery (10 cmds)
- File & search wizardry
- Docker & DevOps tricks
- Process & network tools
- Productivity boosters
- macOS & Linux install guide
No spam. 1 email/week max. Unsubscribe anytime.
Found this guide useful? Check out our free developer tools.
Affiliate disclosure: Some links below are affiliate links โ we may earn a small commission at no extra cost to you. Learn more.
Recommended Tools & Resources
DigitalOcean
$200 credit for new users. Simple, affordable cloud hosting for developers.
GitHub Student Pack
Free access to 100+ developer tools. Perfect for students and new devs.
Vercel
Deploy frontend apps instantly. Free tier is generous for side projects.
DevPlaybook Products
Boilerplates, scripts & AI toolkits to 10x your dev workflow.