Echo #
Echo adalah web framework Go yang memposisikan diri di titik tengah antara produktivitas developer dan performa — lebih opinionated dari net/http standar tetapi lebih dekat ke idiom Go dibandingkan Fiber. Echo dibangun di atas net/http sehingga kompatibel penuh dengan ekosistem middleware dan library Go yang sudah ada. Keunggulan utama Echo terletak pada tiga hal: sistem binding dan validasi yang fleksibel, dukungan custom context yang kuat untuk memperluas fungsionalitas tanpa global state, dan dukungan HTTP/2 serta WebSocket yang mature. Artikel ini membahas semua fitur utama Echo dari instalasi hingga pola organisasi kode yang siap production.
Instalasi #
go get github.com/labstack/echo/v4
Server minimal:
package main
import (
"net/http"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
e.GET("/ping", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"message": "pong"})
})
e.Logger.Fatal(e.Start(":8080"))
}
Echo menggunakannet/httpsebagai transport layer, sehingga semua middleware yang ditulis untukhttp.Handlerbisa digunakan viaecho.WrapMiddleware(). Ini adalah keunggulan Echo dibandingkan Fiber yang tidak kompatibel dengan ekosistemnet/http.
Cara Kerja Request di Echo #
Echo memperkenalkan konsep echo.Context yang membungkus http.Request dan http.ResponseWriter standar sambil menambahkan method-method yang ekspresif. Memahami alur request sangat penting sebelum menulis middleware.
flowchart TD
A([HTTP Request]) --> B["net/http Server"]
B --> C["echo.Context dibuat\n(wraps Request + ResponseWriter)"]
C --> D["Router — radix tree matching"]
D --> E{Route ditemukan?}
E -- Tidak --> F["HTTPErrorHandler\n404 Not Found"]
E -- Ya --> G["Middleware Chain\n(Pre + Group + Route level)"]
G --> H["Handler Utama"]
H --> I["Response ditulis\nke ResponseWriter"]
F --> I
I --> J([Response ke Client])
style A fill:#3b82f6,color:#fff
style J fill:#3b82f6,color:#fff
style F fill:#e05252,color:#fffEcho menggunakan radix tree untuk routing yang memberikan lookup O(log n) bahkan untuk ribuan route. Berbeda dengan Gin yang menggunakan httprouter, Echo membangun tree-nya sendiri dengan dukungan parameter yang lebih fleksibel.
Konfigurasi dan Inisialisasi #
Echo bisa dikustomisasi cukup dalam saat inisialisasi:
e := echo.New()
// Sembunyikan banner startup di production
e.HideBanner = true
e.HidePort = true
// Custom logger
e.Logger.SetLevel(log.INFO)
// Custom HTTP error handler global
e.HTTPErrorHandler = func(err error, c echo.Context) {
code := http.StatusInternalServerError
msg := "terjadi kesalahan internal"
var he *echo.HTTPError
if errors.As(err, &he) {
code = he.Code
if m, ok := he.Message.(string); ok {
msg = m
}
}
// Jangan bocorkan detail error di production
if os.Getenv("APP_ENV") != "production" {
msg = err.Error()
}
c.JSON(code, map[string]interface{}{
"success": false,
"error": msg,
"path": c.Request().URL.Path,
})
}
Routing #
Routing Echo menggunakan konvensi method-per-HTTP-verb yang bersih dan mudah dibaca.
e.GET("/users", listUsers)
e.POST("/users", createUser)
e.PUT("/users/:id", updateUser)
e.PATCH("/users/:id", patchUser)
e.DELETE("/users/:id", deleteUser)
// Semua method
e.Any("/webhook", handleWebhook)
// Method kustom (misalnya untuk WebDAV)
e.Add("PROPFIND", "/dav/*", davHandler)
Parameter Route #
Echo mendukung tiga jenis parameter dalam path:
// Parameter bernama — wajib ada
e.GET("/users/:id", func(c echo.Context) error {
id := c.Param("id")
return c.JSON(http.StatusOK, map[string]string{"id": id})
})
// Wildcard — menangkap semua segmen setelah /files/
e.GET("/files/*", func(c echo.Context) error {
path := c.Param("*")
return c.JSON(http.StatusOK, map[string]string{"path": path})
})
// Beberapa parameter
e.GET("/orgs/:orgID/repos/:repoID", func(c echo.Context) error {
orgID := c.Param("orgID")
repoID := c.Param("repoID")
return c.JSON(http.StatusOK, map[string]string{
"org": orgID,
"repo": repoID,
})
})
Query String #
// GET /search?q=golang&page=2&limit=20
e.GET("/search", func(c echo.Context) error {
q := c.QueryParam("q")
page := c.QueryParam("page")
limit := c.QueryParam("limit")
// Dengan default value
if page == "" {
page = "1"
}
return c.JSON(http.StatusOK, map[string]string{
"q": q, "page": page, "limit": limit,
})
})
Route Groups #
// Grup dengan prefix
api := e.Group("/api")
// v1 — publik
v1 := api.Group("/v1")
v1.GET("/status", statusHandler)
// v2 — memerlukan autentikasi
v2 := api.Group("/v2", authMiddleware)
{
users := v2.Group("/users")
users.GET("", listUsers)
users.POST("", createUser)
users.GET("/:id", getUserByID)
users.PUT("/:id", updateUser)
users.DELETE("/:id", deleteUser)
// Nested group dengan middleware tambahan
admin := v2.Group("/admin", adminOnlyMiddleware)
admin.GET("/metrics", metricsHandler)
admin.GET("/logs", logsHandler)
}
graph TD
A["/api"] --> B["/v1\n(publik)"]
A --> C["/v2\n+ authMiddleware"]
B --> D["GET /status"]
C --> E["/users"]
C --> F["/admin\n+ adminOnly"]
E --> G["GET /"]
E --> H["POST /"]
E --> I["GET /:id"]
E --> J["PUT /:id"]
E --> K["DELETE /:id"]
F --> L["GET /metrics"]
F --> M["GET /logs"]Middleware #
Echo mendukung tiga level pendaftaran middleware: global (semua route), group (sekumpulan route), dan route (satu route spesifik). Ini memberi kontrol granular yang tidak dimiliki framework lain secara built-in.
Level Pendaftaran Middleware #
// 1. Global — berlaku untuk semua route
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// 2. Group — berlaku untuk semua route dalam grup
adminGroup := e.Group("/admin", adminAuth)
// 3. Route — berlaku untuk satu route saja
e.GET("/sensitive", handler, rateLimiter, auditLog)
graph LR
subgraph "Global Middleware"
A["Logger"] --> B["Recover"]
end
subgraph "Routes"
B --> C["GET /health\n(no extra middleware)"]
B --> D["GET /admin/...\n+ adminAuth"]
B --> E["GET /sensitive\n+ rateLimiter + auditLog"]
endMiddleware Bawaan Echo #
Echo menyertakan middleware berkualitas tinggi di subpackage middleware:
import "github.com/labstack/echo/v4/middleware"
// Logger terstruktur
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: `{"time":"${time_rfc3339}","method":"${method}","uri":"${uri}","status":${status},"latency":"${latency_human}"}` + "\n",
}))
// Recovery dari panic
e.Use(middleware.Recover())
// CORS
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://app.example.com"},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAuthorization},
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
}))
// Rate limiter
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20)))
// Gzip compression
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
Level: 5,
}))
// Request ID
e.Use(middleware.RequestID())
// JWT
e.Use(middleware.JWTWithConfig(middleware.JWTConfig{
SigningKey: []byte(os.Getenv("JWT_SECRET")),
}))
// Secure headers (XSS, HSTS, dll)
e.Use(middleware.Secure())
Middleware Kustom #
func AuditMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
start := time.Now()
// Sebelum handler
requestID := c.Response().Header().Get(echo.HeaderXRequestID)
err := next(c) // panggil handler berikutnya
// Setelah handler selesai
log.Printf("audit | id=%s method=%s path=%s status=%d latency=%v",
requestID,
c.Request().Method,
c.Request().URL.Path,
c.Response().Status,
time.Since(start),
)
return err
}
}
Urutan Eksekusi Middleware #
sequenceDiagram
participant Client
participant Logger as LoggerMiddleware
participant Auth as AuthMiddleware
participant H as Handler
Client->>Logger: Request masuk
Logger->>Logger: Catat waktu mulai
Logger->>Auth: next(c)
Auth->>Auth: Validasi JWT
alt Token tidak valid
Auth-->>Logger: return HTTPError 401
Logger->>Logger: Catat status 401
Logger-->>Client: 401 Unauthorized
else Token valid
Auth->>Auth: Set user ke context
Auth->>H: next(c)
H->>H: Proses bisnis
H-->>Auth: return nil
Auth-->>Logger: return nil
Logger->>Logger: Catat status & durasi
Logger-->>Client: 200 Response
endBinding dan Validasi #
Echo memiliki sistem binding yang paling fleksibel di antara tiga framework ini. Satu method Bind() menangani semua sumber data, dan validasi bisa diintegrasikan langsung ke dalam lifecycle binding.
Binding Dasar #
type CreateUserRequest struct {
Name string `json:"name" form:"name" query:"name" validate:"required,min=2,max=100"`
Email string `json:"email" form:"email" query:"email" validate:"required,email"`
Age int `json:"age" form:"age" query:"age" validate:"required,gte=18"`
}
func createUser(c echo.Context) error {
req := new(CreateUserRequest)
if err := c.Bind(req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if err := c.Validate(req); err != nil {
return err // ditangani HTTPErrorHandler
}
return c.JSON(http.StatusCreated, map[string]interface{}{
"success": true,
"data": req,
})
}
Mendaftarkan Validator Global #
Echo tidak menyertakan implementasi validator bawaan — ia mendefinisikan interface echo.Validator yang harus kamu implementasikan:
import "github.com/go-playground/validator/v10"
type CustomValidator struct {
validator *validator.Validate
}
func (cv *CustomValidator) Validate(i interface{}) error {
if err := cv.validator.Struct(i); err != nil {
return echo.NewHTTPError(http.StatusUnprocessableEntity, err.Error())
}
return nil
}
// Daftarkan sekali saat inisialisasi
func main() {
e := echo.New()
e.Validator = &CustomValidator{validator: validator.New()}
// ...
}
Binding dari Sumber Spesifik #
// Hanya dari path params
type UserURI struct {
ID uint `param:"id" validate:"required"`
}
req := new(UserURI)
if err := c.Bind(req); err != nil { ... }
// Hanya dari query string
type PaginationQuery struct {
Page int `query:"page"`
Limit int `query:"limit"`
}
// Binding manual dari query dengan default
page, _ := strconv.Atoi(c.QueryParam("page"))
limit, _ := strconv.Atoi(c.QueryParam("limit"))
if page <= 0 { page = 1 }
if limit <= 0 { limit = 20 }
flowchart LR
A["JSON Body"] --> E["c.Bind(&req)"]
B["Form Data"] --> E
C["Query String"] --> E
D["Path Params\n:id"] --> E
E --> F["Struct Go\nterisi"]
F --> G["c.Validate(&req)"]
G --> H{CustomValidator}
H -- Gagal --> I["422 HTTPError"]
H -- Lolos --> J["Handler Logic"]Custom Context #
Custom context adalah fitur Echo yang paling membedakannya dari Gin dan Fiber. Alih-alih menyimpan data di map string (c.Set/c.Get), kamu bisa memperluas echo.Context dengan field dan method yang bertipe — menghilangkan type assertion sepenuhnya.
Mendefinisikan Custom Context #
// Definisi custom context
type AppContext struct {
echo.Context
UserID int
UserRole string
TraceID string
}
// Middleware yang menyuntikkan custom context
func AppContextMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
traceID := c.Response().Header().Get(echo.HeaderXRequestID)
cc := &AppContext{
Context: c,
TraceID: traceID,
}
return next(cc)
}
}
// Middleware autentikasi yang mengisi field context
func AuthMiddlewareWithContext(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
cc := c.(*AppContext) // type assertion hanya sekali di sini
token := c.Request().Header.Get("Authorization")
userID, role, err := validateToken(token)
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "token tidak valid")
}
cc.UserID = userID
cc.UserRole = role
return next(cc)
}
}
Menggunakan Custom Context di Handler #
// ANTI-PATTERN: type assertion di setiap handler
func getProfile(c echo.Context) error {
userID, ok := c.Get("userID").(int) // type assertion berulang
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized)
}
// ...
}
// BENAR: custom context — tidak ada type assertion di handler
func getProfile(c echo.Context) error {
cc := c.(*AppContext) // satu type assertion, tipe sudah pasti
// Akses langsung tanpa casting
userID := cc.UserID
userRole := cc.UserRole
traceID := cc.TraceID
return c.JSON(http.StatusOK, map[string]interface{}{
"userID": userID,
"userRole": userRole,
"traceID": traceID,
})
}
flowchart TD
A["echo.Context\n(bawaan)"] -->|"Embedding"| B["AppContext\n+ UserID int\n+ UserRole string\n+ TraceID string"]
C["AppContextMiddleware"] -->|"Bungkus c"| B
D["AuthMiddleware"] -->|"Isi UserID & UserRole"| B
B --> E["Handler\ncc := c.(*AppContext)\ncc.UserID — tanpa type assertion"]Response #
Echo menyediakan method response yang ekspresif dan konsisten karena semua handler mengembalikan error.
JSON dan Format Lain #
// JSON
return c.JSON(http.StatusOK, user)
// JSON dengan pretty print (untuk debugging)
return c.JSONPretty(http.StatusOK, user, " ")
// JSONP (untuk cross-origin dari browser lama)
return c.JSONP(http.StatusOK, "callback", user)
// XML
return c.XML(http.StatusOK, user)
// String
return c.String(http.StatusOK, "Hello, World!")
// HTML
return c.HTML(http.StatusOK, "<h1>Hello</h1>")
// File
return c.File("/path/to/report.pdf")
return c.Attachment("/path/to/report.pdf", "laporan.pdf")
// Redirect
return c.Redirect(http.StatusMovedPermanently, "https://example.com")
// No content
return c.NoContent(http.StatusNoContent)
Response Helper Terpusat #
// pkg/response/response.go
package response
import (
"net/http"
"github.com/labstack/echo/v4"
)
type Response struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
type Meta struct {
Page int `json:"page"`
Limit int `json:"limit"`
TotalItems int `json:"total_items"`
TotalPages int `json:"total_pages"`
}
type PaginatedResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data"`
Meta Meta `json:"meta"`
}
func OK(c echo.Context, data interface{}) error {
return c.JSON(http.StatusOK, Response{Success: true, Data: data})
}
func Created(c echo.Context, data interface{}) error {
return c.JSON(http.StatusCreated, Response{Success: true, Data: data})
}
func Paginated(c echo.Context, data interface{}, meta Meta) error {
return c.JSON(http.StatusOK, PaginatedResponse{
Success: true, Data: data, Meta: meta,
})
}
func BadRequest(c echo.Context, msg string) error {
return echo.NewHTTPError(http.StatusBadRequest, msg)
}
func NotFound(c echo.Context, resource string) error {
return echo.NewHTTPError(http.StatusNotFound, resource+" tidak ditemukan")
}
Upload File #
func uploadFile(c echo.Context) error {
// File tunggal
file, err := c.FormFile("file")
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "file tidak ditemukan")
}
// Validasi ukuran
if file.Size > 10*1024*1024 { // 10 MB
return echo.NewHTTPError(http.StatusRequestEntityTooLarge,
"ukuran file melebihi batas 10 MB")
}
// Validasi tipe
ext := strings.ToLower(filepath.Ext(file.Filename))
allowed := map[string]bool{".jpg": true, ".png": true, ".pdf": true}
if !allowed[ext] {
return echo.NewHTTPError(http.StatusBadRequest, "tipe file tidak diizinkan")
}
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
// Simpan dengan nama unik
filename := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
dst, err := os.Create(filepath.Join("uploads", filename))
if err != nil {
return err
}
defer dst.Close()
if _, err = io.Copy(dst, src); err != nil {
return err
}
return c.JSON(http.StatusOK, map[string]interface{}{
"filename": filename,
"size": file.Size,
})
}
WebSocket #
Echo menyediakan dukungan WebSocket via package golang.org/x/net/websocket atau library pihak ketiga seperti gorilla/websocket.
go get golang.org/x/net/websocket
import "golang.org/x/net/websocket"
func chatHandler(c echo.Context) error {
websocket.Handler(func(ws *websocket.Conn) {
defer ws.Close()
// Ambil data dari custom context jika perlu
// cc := c.(*AppContext)
for {
var msg string
if err := websocket.Message.Receive(ws, &msg); err != nil {
break // client disconnect
}
response := fmt.Sprintf("echo: %s", msg)
if err := websocket.Message.Send(ws, response); err != nil {
break
}
}
}).ServeHTTP(c.Response(), c.Request())
return nil
}
e.GET("/ws/chat", chatHandler)
WebSocket dengan Gorilla (Direkomendasikan) #
go get github.com/gorilla/websocket
import "github.com/gorilla/websocket"
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
// Validasi origin di production
return r.Header.Get("Origin") == "https://app.example.com"
},
}
func chatHandler(c echo.Context) error {
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
return err
}
defer ws.Close()
for {
msgType, msg, err := ws.ReadMessage()
if err != nil {
break
}
if err := ws.WriteMessage(msgType, msg); err != nil {
break
}
}
return nil
}
sequenceDiagram
participant Client
participant Echo as Echo Router
participant WS as WebSocket Handler
Client->>Echo: GET /ws/chat\nUpgrade: websocket
Echo->>WS: Route ke handler
WS->>WS: upgrader.Upgrade()
WS->>Client: 101 Switching Protocols
loop Koneksi aktif
Client->>WS: ReadMessage()
WS->>WS: Proses pesan
WS->>Client: WriteMessage()
end
Client->>WS: Close frame
WS->>WS: ws.Close()HTTP/2 #
Echo mendukung HTTP/2 secara native melalui TLS. Tidak perlu konfigurasi tambahan — cukup jalankan server dengan HTTPS.
// HTTP/2 aktif otomatis saat menggunakan StartTLS
e.Logger.Fatal(e.StartTLS(":443", "cert.pem", "key.pem"))
// Atau dengan auto-TLS via Let's Encrypt
e.Logger.Fatal(e.StartAutoTLS(":443"))
// Untuk development dengan self-signed certificate
// generate dulu: openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
e.Logger.Fatal(e.StartTLS(":8443", "cert.pem", "key.pem"))
HTTP/2 memberikan keuntungan multiplexing (banyak request dalam satu koneksi TCP), header compression, dan server push. Untuk API yang melayani banyak resource kecil secara paralel, HTTP/2 bisa mengurangi latency secara signifikan dibandingkan HTTP/1.1.
Error Handling #
Echo menggunakan echo.HTTPError sebagai tipe error standar yang membawa status code dan pesan. Semua error yang dikembalikan handler akan ditangani HTTPErrorHandler global.
// Kembalikan error dengan status code yang tepat
return echo.NewHTTPError(http.StatusNotFound, "user tidak ditemukan")
return echo.NewHTTPError(http.StatusBadRequest, "format email tidak valid")
return echo.NewHTTPError(http.StatusForbidden, "akses ditolak")
// Error internal — jangan bocorkan detail ke client
func getUserByID(c echo.Context) error {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "ID harus berupa angka")
}
user, err := userService.GetByID(id)
if err != nil {
if errors.Is(err, ErrNotFound) {
return echo.NewHTTPError(http.StatusNotFound, "user tidak ditemukan")
}
// Log detail error, tapi jangan kirim ke client
c.Logger().Errorf("GetByID failed: %v", err)
return echo.ErrInternalServerError
}
return c.JSON(http.StatusOK, user)
}
flowchart TD
A["Handler return error"] --> B{echo.HTTPError?}
B -- Ya --> C["Ambil Code & Message\ndari HTTPError"]
B -- Tidak --> D["Code: 500\nMessage: Internal Server Error"]
C --> E["HTTPErrorHandler\nformat JSON response"]
D --> E
E --> F{APP_ENV == production?}
F -- Ya --> G["Sembunyikan detail error"]
F -- Tidak --> H["Tampilkan detail error\nuntuk debugging"]
G --> I([Response ke Client])
H --> IGraceful Shutdown #
func main() {
e := echo.New()
e.HideBanner = true
// ... setup routes dan middleware
// Jalankan server di goroutine terpisah
go func() {
if err := e.Start(":8080"); err != nil && err != http.ErrServerClosed {
e.Logger.Fatal("server error:", err)
}
}()
// Tunggu sinyal interrupt
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
<-quit
// Graceful shutdown dengan timeout 10 detik
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := e.Shutdown(ctx); err != nil {
e.Logger.Fatal("shutdown error:", err)
}
e.Logger.Info("server berhenti.")
}
Struktur Project yang Direkomendasikan #
myapp/
├── main.go
├── internal/
│ ├── handler/
│ │ ├── user.go
│ │ └── product.go
│ ├── middleware/
│ │ ├── auth.go
│ │ ├── context.go ← custom context didefinisikan di sini
│ │ └── error.go
│ ├── service/
│ │ └── user.go
│ └── repository/
│ └── user.go
├── pkg/
│ ├── response/
│ │ └── response.go
│ └── validator/
│ └── validator.go ← CustomValidator diimplementasikan di sini
└── router/
└── router.go
Setup Router #
// router/router.go
package router
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"myapp/internal/handler"
mw "myapp/internal/middleware"
"myapp/pkg/validator"
)
func Setup(userHandler *handler.UserHandler) *echo.Echo {
e := echo.New()
e.HideBanner = true
// Validator global
e.Validator = validator.New()
// Custom error handler
e.HTTPErrorHandler = mw.ErrorHandler
// Middleware global
e.Use(middleware.Recover())
e.Use(middleware.RequestID())
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: `{"time":"${time_rfc3339}","id":"${id}","method":"${method}","uri":"${uri}","status":${status}}` + "\n",
}))
e.Use(mw.AppContextMiddleware) // injeksi custom context
// Health check
e.GET("/health", func(c echo.Context) error {
return c.JSON(200, map[string]string{"status": "ok"})
})
// API routes
api := e.Group("/api/v1")
api.Use(mw.AuthMiddlewareWithContext)
{
users := api.Group("/users")
users.GET("", userHandler.List)
users.POST("", userHandler.Create)
users.GET("/:id", userHandler.GetByID)
users.PUT("/:id", userHandler.Update)
users.DELETE("/:id", userHandler.Delete)
}
return e
}
Perbandingan Echo vs Gin vs Fiber #
Setelah membahas ketiganya, berikut perbedaan utama yang membantu kamu memilih:
| Aspek | Echo | Gin | Fiber |
|---|---|---|---|
| Transport | net/http | net/http | Fasthttp |
| Performa | Tinggi | Tinggi | Tertinggi |
| Kompatibilitas net/http | ✓ Penuh | ✓ Penuh | ✗ Partial (adaptor) |
| Custom context | ✓ Built-in | ✗ (pakai c.Set/Get) | ✗ (pakai c.Locals) |
| Validator bawaan | Interface saja | ✓ via tag binding | ✗ (manual) |
| WebSocket | ✓ Mature | Terbatas | ✓ via package |
| HTTP/2 | ✓ Native | ✗ | ✗ |
| Middleware level | Global/Group/Route | Global/Group | Global/Group/Route |
Kapan Tidak Menggunakan Echo #
Tetap gunakan Echo jika:
✓ Butuh custom context yang type-safe tanpa boilerplate
✓ HTTP/2 adalah kebutuhan (API yang melayani banyak resource paralel)
✓ Butuh kompatibilitas penuh dengan ekosistem net/http
✓ Tim menginginkan framework yang dekat dengan idiom Go
Pertimbangkan Gin jika:
✗ Tim sudah familier dengan Gin dan tidak butuh custom context
✗ Binding dengan validasi terintegrasi (tag binding) lebih diprioritaskan
Pertimbangkan Fiber jika:
✗ Performa raw adalah prioritas absolut
✗ Background tim adalah Express.js / Node.js
Pertimbangkan net/http standar jika:
✗ Aplikasi sangat sederhana dan tidak butuh abstraksi framework
Ringkasan #
- Custom context — keunggulan utama Echo; extend
echo.Contextdengan field bertipe untuk menghilangkan type assertion di setiap handler.c.Bind()universal — satu method menangani JSON, XML, form, query, dan path param berdasarkan Content-Type dan tag struct.- Validator via interface — implementasikan
echo.Validatordengango-playground/validator; daftarkan sekali die.Validatordan panggilc.Validate()di handler.- Tiga level middleware — global (
e.Use), group (g.Use), dan per-route (e.GET("/path", handler, mw1, mw2)); gunakan ini untuk kontrol granular.echo.NewHTTPError— kembalikan error dengan status code yang tepat; biarkanHTTPErrorHandlerglobal yang memformatnya secara konsisten.- HTTP/2 native — aktif otomatis saat menggunakan
StartTLSatauStartAutoTLS; tidak perlu konfigurasi tambahan.- Kompatibel net/http — semua middleware
http.Handlerbisa digunakan viaecho.WrapMiddleware(); tidak seperti Fiber yang butuh adapter khusus.- Graceful shutdown — gunakan
e.Shutdown(ctx)dengan context ber-timeout agar request yang sedang berjalan selesai sebelum server mati.