Echo
High-performance and minimal Go web framework. Provides more built-in functionality, suitable for larger applications.
GitHub Overview
labstack/echo
High performance, minimalist Go web framework
Topics
Star History
Framework
Echo
Overview
Echo is a high-performance, minimalist web framework written in Go. It specializes in building RESTful APIs and web applications.
Details
Echo was developed by LabStack in 2015, designed as a Go web framework that emphasizes simplicity and performance. It features an optimized HTTP router, rich middleware ecosystem, and intuitive API that enables developers to rapidly build high-performance web applications. Echo uses a tree-based routing algorithm for efficient route matching. The middleware system is hierarchical, allowing application at global, group, and route levels. It includes automatic binding for JSON, XML, and form data, centralized error handling, HTTP/2 support, and automatic TLS with Let's Encrypt. Despite its lightweight nature, it provides enterprise-level features, making it ideal for microservice architectures and API-first development approaches.
Pros and Cons
Pros
- High Performance: Optimized router and lightweight design for fast response times
- Simple Learning Curve: Intuitive API with clear documentation
- Rich Middleware: Built-in logging, authentication, CORS, compression, and more
- Flexible Routing: Support for parameters, wildcards, and route grouping
- Automatic Binding: Automatic conversion of JSON and form data
- HTTP/2 Support: Modern HTTP protocol support
- Active Community: Active development and community support
Cons
- Limited Features: More limited functionality compared to full-stack frameworks
- Go Language Dependency: Go-specific learning curve and development environment constraints
- No Built-in ORM: Requires separate libraries for database operations
- Template Limitations: Basic built-in template engine functionality
- Ecosystem: Relatively fewer framework-specific tools
Key Links
- Echo Official Site
- Echo Official Documentation
- Echo GitHub Repository
- Echo Middleware
- Echo Examples
- Echo Community
Code Examples
Hello World
package main
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
// Create Echo instance
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Routes
e.GET("/", hello)
// Start server
e.Logger.Fatal(e.Start(":1323"))
}
// Handler function
func hello(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
}
Routing and Parameters
package main
import (
"net/http"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
// Basic routes
e.GET("/", home)
e.POST("/users", createUser)
e.GET("/users/:id", getUser)
e.PUT("/users/:id", updateUser)
e.DELETE("/users/:id", deleteUser)
// Wildcard route
e.GET("/files/*", getFile)
// Query parameters
e.GET("/search", search)
e.Logger.Fatal(e.Start(":1323"))
}
func home(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"message": "Welcome to Echo API",
})
}
func getUser(c echo.Context) error {
// Get path parameter
id := c.Param("id")
return c.JSON(http.StatusOK, map[string]string{
"id": id,
"name": "User " + id,
})
}
func search(c echo.Context) error {
// Get query parameters
query := c.QueryParam("q")
page := c.QueryParam("page")
if query == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Search query is required",
})
}
return c.JSON(http.StatusOK, map[string]interface{}{
"query": query,
"page": page,
"results": []string{"Result 1", "Result 2", "Result 3"},
})
}
func getFile(c echo.Context) error {
// Get wildcard parameter
filepath := c.Param("*")
return c.JSON(http.StatusOK, map[string]string{
"filepath": filepath,
})
}
JSON Data Binding and Validation
package main
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/go-playground/validator/v10"
)
type User struct {
ID int `json:"id"`
Name string `json:"name" validate:"required,min=2,max=50"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"min=0,max=120"`
}
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()}
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.POST("/users", createUser)
e.GET("/users/:id", getUser)
e.Logger.Fatal(e.Start(":1323"))
}
func createUser(c echo.Context) error {
user := new(User)
// Bind JSON data
if err := c.Bind(user); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid JSON format",
})
}
// Validate
if err := c.Validate(user); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": err.Error(),
})
}
// User creation logic (actual DB save, etc.)
user.ID = 123 // Mock ID
return c.JSON(http.StatusCreated, user)
}
func getUser(c echo.Context) error {
// Mock user data
user := User{
ID: 1,
Name: "John Doe",
Email: "[email protected]",
Age: 30,
}
return c.JSON(http.StatusOK, user)
}
Using Middleware
package main
import (
"net/http"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
// Global middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())
// Rate limiting
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20)))
// Security headers
e.Use(middleware.Secure())
// Gzip compression
e.Use(middleware.Gzip())
// Request timeout
e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
Timeout: 30 * time.Second,
}))
// Public routes
e.GET("/", publicHandler)
e.POST("/register", registerHandler)
// API group requiring authentication
api := e.Group("/api")
api.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
if username == "admin" && password == "secret" {
return true, nil
}
return false, nil
}))
api.GET("/users", listUsers)
api.POST("/users", createUser)
// Custom middleware
e.Use(customLogger)
e.Logger.Fatal(e.Start(":1323"))
}
func customLogger(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
start := time.Now()
// Execute next handler
err := next(c)
// Log output
c.Logger().Infof("Method: %s, URI: %s, Status: %d, Latency: %v",
c.Request().Method,
c.Request().RequestURI,
c.Response().Status,
time.Since(start),
)
return err
}
}
func publicHandler(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"message": "Public endpoint",
})
}
func registerHandler(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"message": "User registration",
})
}
func listUsers(c echo.Context) error {
return c.JSON(http.StatusOK, []map[string]interface{}{
{"id": 1, "name": "User 1"},
{"id": 2, "name": "User 2"},
})
}
Error Handling
package main
import (
"errors"
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
type HTTPError struct {
Code int `json:"code"`
Message string `json:"message"`
}
func main() {
e := echo.New()
// Custom error handler
e.HTTPErrorHandler = customHTTPErrorHandler
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.GET("/", successHandler)
e.GET("/error", errorHandler)
e.GET("/panic", panicHandler)
e.GET("/users/:id", getUserWithError)
e.Logger.Fatal(e.Start(":1323"))
}
func customHTTPErrorHandler(err error, c echo.Context) {
var code int
var message string
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
message = he.Message.(string)
} else {
code = http.StatusInternalServerError
message = "Internal Server Error"
}
// Log error
c.Logger().Error(err)
// JSON error response
if !c.Response().Committed {
if c.Request().Method == http.MethodHead {
err = c.NoContent(code)
} else {
err = c.JSON(code, HTTPError{
Code: code,
Message: message,
})
}
if err != nil {
c.Logger().Error(err)
}
}
}
func successHandler(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"message": "Success",
})
}
func errorHandler(c echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, "Custom error message")
}
func panicHandler(c echo.Context) error {
panic("A panic occurred")
}
func getUserWithError(c echo.Context) error {
id := c.Param("id")
// Business logic error
if id == "999" {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
// System error
if id == "500" {
return errors.New("Database connection error")
}
return c.JSON(http.StatusOK, map[string]string{
"id": id,
"name": "User " + id,
})
}
File Upload and Static File Serving
package main
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Static file serving
e.Static("/static", "assets")
// File upload
e.POST("/upload", uploadFile)
e.POST("/upload/multiple", uploadMultipleFiles)
// File download
e.GET("/download/:filename", downloadFile)
e.Logger.Fatal(e.Start(":1323"))
}
func uploadFile(c echo.Context) error {
// Get file from form
file, err := c.FormFile("file")
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Failed to get file")
}
// File size check (10MB limit)
if file.Size > 10<<20 {
return echo.NewHTTPError(http.StatusBadRequest, "File too large (max 10MB)")
}
// Open file
src, err := file.Open()
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to read file")
}
defer src.Close()
// Create upload directory if it doesn't exist
uploadDir := "uploads"
if _, err := os.Stat(uploadDir); os.IsNotExist(err) {
err = os.Mkdir(uploadDir, 0755)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create upload directory")
}
}
// Create destination file
dst, err := os.Create(filepath.Join(uploadDir, file.Filename))
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create file")
}
defer dst.Close()
// Copy file
if _, err = io.Copy(dst, src); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to save file")
}
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "File upload successful",
"filename": file.Filename,
"size": file.Size,
})
}
func uploadMultipleFiles(c echo.Context) error {
// Parse multipart form
form, err := c.MultipartForm()
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Failed to parse multipart form")
}
files := form.File["files"]
var uploadedFiles []string
for _, file := range files {
// File size check
if file.Size > 10<<20 {
continue // Skip files that are too large
}
src, err := file.Open()
if err != nil {
continue
}
dst, err := os.Create(filepath.Join("uploads", file.Filename))
if err != nil {
src.Close()
continue
}
if _, err = io.Copy(dst, src); err != nil {
src.Close()
dst.Close()
continue
}
src.Close()
dst.Close()
uploadedFiles = append(uploadedFiles, file.Filename)
}
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "File upload completed",
"uploaded_files": uploadedFiles,
"count": len(uploadedFiles),
})
}
func downloadFile(c echo.Context) error {
filename := c.Param("filename")
filePath := filepath.Join("uploads", filename)
// Check file existence
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return echo.NewHTTPError(http.StatusNotFound, "File not found")
}
// Set Content-Disposition header to prompt download
c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
return c.File(filePath)
}