express() → chi.NewRouter() (создание роутера)app.get('/users', handler) → r.Get("/users", handler) (маршрут)app.use(middleware) → r.Use(middleware) (глобальная middleware)router.group('/api', ...) → r.Route("/api", func(r chi.Router) {...}) (группировка)req.params.id → chi.URLParam(r, "id") (параметры пути)morgan / helmet → chi-middleware (логи, безопасность)go-chi/chigo mod init myapp
go get github.com/go-chi/chi/v5
go get github.com/go-chi/cors # CORS middleware
go get github.com/go-chi/httprate # rate limiter
go get github.com/go-chi/chi/middleware # основные middleware (встроены в chi)
Аналог в Node.js: npm install express cors express-rate-limit
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
// Глобальные middleware
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.RequestID)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, chi!"))
})
http.ListenAndServe(":3000", r)
}
Сравнение с Express: chi.NewRouter() — это как express(), а r.Use(...) — как app.use(...). Middleware в chi применяются в порядке объявления.
r.Get("/users", listUsers)
r.Post("/users", createUser)
r.Put("/users/{id}", updateUser) // {id} — параметр пути
r.Delete("/users/{id}", deleteUser)
r.Patch("/users/{id}", patchUser)
r.Options("/users/{id}", optionsUser)
r.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
w.Write([]byte("User ID: " + id))
})
// С регулярным выражением:
r.Get("/users/{id:[0-9]+}", func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id") // гарантированно число
})
// Несколько параметров:
r.Get("/posts/{year}/{month}/{slug}", func(w http.ResponseWriter, r *http.Request) {
year := chi.URLParam(r, "year")
month := chi.URLParam(r, "month")
slug := chi.URLParam(r, "slug")
})
Аналог в Express: req.params.id → chi.URLParam(r, "id")
r.Get("/search", func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query().Get("q") // ?q=golang
page := r.URL.Query().Get("page") // ?page=2
w.Write([]byte(fmt.Sprintf("Search: %s, Page: %s", q, page)))
})
chi поддерживает вложенные роутеры через r.Route() — аналог express.Router().
r.Route("/api", func(r chi.Router) {
r.Get("/status", statusHandler) // GET /api/status
r.Get("/version", versionHandler) // GET /api/version
})
r.Route("/api/v1", func(r chi.Router) {
r.Route("/users", func(r chi.Router) {
r.Get("/", listUsers) // GET /api/v1/users
r.Post("/", createUser) // POST /api/v1/users
r.Route("/{id}", func(r chi.Router) {
r.Get("/", getUser) // GET /api/v1/users/123
r.Put("/", updateUser) // PUT /api/v1/users/123
r.Delete("/", deleteUser) // DELETE /api/v1/users/123
})
})
})
r.Route("/admin", func(r chi.Router) {
// Вся группа /admin требует авторизации
r.Use(authMiddleware)
r.Use(adminOnlyMiddleware)
r.Get("/dashboard", dashboardHandler) // GET /admin/dashboard
r.Get("/stats", statsHandler) // GET /admin/stats
})
Аналог в Express:
const adminRouter = express.Router();
adminRouter.use(authMiddleware);
adminRouter.use(adminOnlyMiddleware);
adminRouter.get('/dashboard', dashboardHandler);
app.use('/admin', adminRouter);
chi/middleware)| Middleware | Назначение | Аналог в Node.js |
|---|---|---|
Logger | Логирование запросов (метод, путь, статус, длительность) | morgan |
Recoverer | Перехват паник, возврат 500 | express-async-errors |
RequestID | Добавляет X-Request-ID к каждому запросу | express-request-id |
RealIP | Определение реального IP через заголовки (X-Forwarded-For) | express-X-Forwarded-For |
Timeout | Таймаут запроса (контекст отмены) | connect-timeout |
Compress | Gzip-сжатие ответов | compression |
Heartbeat | Endpoint для health-check | — |
CleanPath | Нормализация пути (удаление слешей) | — |
RedirectSlashes | Редирект при конечном слеше | express-slash |
r := chi.NewRouter()
// Порядок важен! Первый = внешний
r.Use(middleware.RequestID) // 1. ID запроса
r.Use(middleware.RealIP) // 2. Реальный IP
r.Use(middleware.Logger) // 3. Логирование
r.Use(middleware.Recoverer) // 4. Восстановление после паник
r.Use(middleware.Timeout(30 * time.Second)) // 5. Таймаут 30с
// Middleware для конкретного маршрута
r.Get("/health", middleware.Heartbeat("/health"))
r.Use(A) // снаружи — выполняется первой на входе, последней на выходе
r.Use(B) // |
r.Use(C) // внутри — выполняется последней на входе, первой на выходе
// Схема для запроса:
// Request → A → B → C → Handler → C → B → A → Response
Аналог в Express: точно так же — app.use(A) → app.use(B) → app.use(C), и они выполняются как onion-модель.
func customLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Обёртка для ResponseWriter, чтобы поймать статус
wr := &responseWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(wr, r)
log.Printf("[%s] %s %s %d %v",
r.Method, r.URL.Path, r.RemoteAddr, wr.status, time.Since(start))
})
}
type responseWriter struct {
http.ResponseWriter
status int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.status = code
rw.ResponseWriter.WriteHeader(code)
}
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Валидация токена...
userID := "user_123"
// Передаём данные в контекст
ctx := context.WithValue(r.Context(), "user_id", userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// Использование:
r.With(authMiddleware).Get("/profile", profileHandler)
// Или глобально:
r.Use(authMiddleware)
import "github.com/go-chi/cors"
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"https://example.com", "http://localhost:3000"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
ExposedHeaders: []string{"Link"},
AllowCredentials: false,
MaxAge: 300, // seconds
}))
Аналог в Express: npm install cors → app.use(cors({ origin: '...' }))
import "github.com/go-chi/httprate"
r.Use(httprate.LimitByIP(100, 1*time.Minute))
// 100 запросов в минуту на IP
// Или по ключу:
r.Use(httprate.LimitBy(10, 1*time.Minute, httprate.KeyByIP))
func rateLimiter(maxRequests int, per time.Duration) func(http.Handler) http.Handler {
visitors := make(map[string]int)
var mu sync.Mutex
// Очистка каждые per
go func() {
for {
time.Sleep(per)
mu.Lock()
visitors = make(map[string]int)
mu.Unlock()
}
}()
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr
mu.Lock()
visitors[ip]++
count := visitors[ip]
mu.Unlock()
if count > maxRequests {
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}
chi расширяет стандартный context.Context, позволяя передавать данные между middleware и обработчиками.
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), "user", &User{ID: 1, Name: "Алиса"})
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func handler(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value("user").(*User)
// или
if user, ok := r.Context().Value("user").(*User); ok {
json.NewEncoder(w).Encode(user)
}
}
chi также хранит параметры маршрута в контексте:
// Внутреннее устройство chi.URLParam:
func URLParam(r *http.Request, key string) string {
return chi.RouteContext(r.Context()).URLParam(key)
}
chi позволяет монтировать один роутер в другой — аналог app.use('/prefix', subRouter) в Express.
func main() {
r := chi.NewRouter()
// Монтируем под-роутер
r.Mount("/api/v1", apiRouter())
r.Mount("/admin", adminRouter())
http.ListenAndServe(":3000", r)
}
func apiRouter() http.Handler {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Get("/users", listUsers)
r.Post("/users", createUser)
return r
}
func adminRouter() http.Handler {
r := chi.NewRouter()
r.Use(authMiddleware)
r.Get("/dashboard", dashboardHandler)
r.Get("/stats", statsHandler)
return r
}
r.With() создаёт временную группу с дополнительными middleware.
r.With(authMiddleware, adminMiddleware).Route("/admin", func(r chi.Router) {
r.Get("/dashboard", dashboardHandler)
r.Get("/stats", statsHandler)
})
// Эквивалентно:
r.Group(func(r chi.Router) {
r.Use(authMiddleware)
r.Use(adminMiddleware)
r.Get("/admin/dashboard", dashboardHandler)
r.Get("/admin/stats", statsHandler)
})
r := chi.NewRouter()
// Глобальные middleware
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(30 * time.Second))
r.Use(cors.Handler(cors.Options{/*...*/}))
// Health check
r.Get("/health", healthHandler)
r.Get("/ready", readinessHandler)
// API v1
r.Route("/api/v1", func(r chi.Router) {
r.Use(rateLimiter)
r.Route("/users", func(r chi.Router) {
r.Get("/", listUsers)
r.Post("/", createUser)
r.Route("/{id}", func(r chi.Router) {
r.Use(authMiddleware)
r.Get("/", getUser)
r.Put("/", updateUser)
r.Delete("/", deleteUser)
})
})
r.Route("/posts", func(r chi.Router) {
r.Use(authMiddleware)
r.Get("/", listPosts)
r.Post("/", createPost)
r.Route("/{id}", func(r chi.Router) {
r.Get("/", getPost)
r.Put("/", updatePost)
r.Delete("/", deletePost)
})
})
})
// API v2 (новая версия)
r.Mount("/api/v2", v2Router())
func errorHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Обёртка для перехвата ошибок из хендлеров
wr := &errorResponseWriter{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(wr, r)
if wr.err != nil {
log.Printf("Handler error: %v", wr.err)
// Можно отправить ошибку в Sentry/DataDog
}
})
}
type errorResponseWriter struct {
http.ResponseWriter
status int
err error
}
func RequireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Извлекаем токен
token := extractToken(r)
if token == "" {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
// Валидируем
user, err := validateJWT(token)
if err != nil {
http.Error(w, `{"error":"invalid token"}`, http.StatusUnauthorized)
return
}
// Кладём в контекст
ctx := context.WithValue(r.Context(), "user", user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func RequireRole(role string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value("user").(*User)
if !user.HasRole(role) {
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
// Использование:
r.With(RequireAuth, RequireRole("admin")).Get("/admin", adminHandler)
express()app.use() в Express)