Overview
Echo occupies the same batteries-included niche as Gin — its own context type, radix-tree
routing, binding, a broad first-party middleware set — but with two design choices that
distinguish it: handlers return errors that flow to one centralized
HTTPErrorHandler, and echo.Context is an interface, not a struct. The first is
Echo's best idea and the main reason to pick it over Gin; the second makes decoration and
test doubles cleaner. Like Gin, the framework is ServeHTTP-compatible at the outer edge
while replacing stdlib idioms inside.
Key points
echo.Contextis an interface bundling request, response, path/query params, binding, rendering, and a value store; being an interface, it can be wrapped in custom middleware (a common pattern for adding typed accessors per project).- Handlers return
error—func(c echo.Context) error— instead of writing error responses inline. Business code stays on the happy path; failures become values. - Centralized
HTTPErrorHandler: every returned error funnels into one function that maps it to a response.echo.NewHTTPError(404, "no such item")carries status + message; anything else defaults to 500. One place to shape the error envelope, log, and hide internals — Gin has no equivalent; error rendering there is per-handler discipline. - Binding & validation hooks:
c.Bind(&in)populates from JSON/XML/form/query/path; validation is pluggable — sete.Validatorto anything implementingValidate(i any) error, conventionally a go-playground/validator wrapper. Explicitly a hook, not a bundled dependency. - Middleware:
echo.MiddlewareFuncchains aroundHandlerFuncs; the first-party set covers Logger, Recover, CORS, JWT, RateLimiter, Gzip, BodyLimit, Secure headers. - Stdlib-wrap adapters ship in the box:
echo.WrapHandler(http.Handler)andecho.WrapMiddleware(func(http.Handler) http.Handler)pull stdlib-shaped code into Echo's chain — friendlier to thehttp.Handlerecosystem than Gin, though the wrapping still costs the stdlib code access to Echo's context store. - Route groups:
g := e.Group("/admin", authMW)scope prefix and middleware; routes are named and reversible (e.Reverse) for URL generation. - Vs Gin: near-identical performance and feature envelope; choose Echo for the error model and interface-based context, Gin for the larger community and middleware ecosystem. Migrating between them is mechanical but touches every handler signature.
Details
The error-handling pattern in practice
e := echo.New()
e.HTTPErrorHandler = func(err error, c echo.Context) {
code, msg := http.StatusInternalServerError, "internal error"
var he *echo.HTTPError
if errors.As(err, &he) {
code, msg = he.Code, fmt.Sprint(he.Message)
}
if !c.Response().Committed {
_ = c.JSON(code, map[string]string{"error": msg})
}
slog.Error("request failed", "path", c.Path(), "err", err)
}
Domain errors translate once: an errors.Is(err, storage.ErrNotFound) branch in the
error handler replaces if err != nil { c.JSON(404, ...) } scattered through handlers.
This is the pattern teams bolt onto Gin by hand (middleware inspecting c.Errors); in
Echo it is the paved road.
Binding + validation wiring
type Validator struct{ v *validator.Validate }
func (cv *Validator) Validate(i any) error { return cv.v.Struct(i) }
e.Validator = &Validator{v: validator.New()}
e.POST("/items", func(c echo.Context) error {
var in CreateItem
if err := c.Bind(&in); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if err := c.Validate(&in); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return c.JSON(http.StatusCreated, in)
})
Related
- Go web frameworks — the landscape — parent overview; Echo's place in the field.
- Gin deep dive — the direct rival; same niche, per-handler error discipline instead of a central handler.
- net/http deep dive — the stdlib contract Echo wraps and adapts to.