返回 Skill 列表
extension
分类: 开发与工程无需 API Key

chi

Chi 是一个轻量级的 Go HTTP 路由器。涵盖了路由、中间件、上下文和模式。用于惯用且与标准库兼容的 Go API。使用场景:用户提到“chi”、“go-chi”、“轻量级 go 路由器”、“标准库 go 路由器”,询问关于“chi 中间件”、“chi 路由器”、“chi 上下文”、“惯用 go api”、“net/http 兼容路由器”、“chi 模式”。不应用于:Gin 项目 - 应该使用 `gin`,Echo 项目 - 应该使用 `echo`,Fiber 项目 - 应该使用 `fiber`,非 Go 后端。

person作者: jakexiaohubgithub

Chi Core Knowledge

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: chi for comprehensive documentation.

Basic Setup

package main

import (
    "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("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, World!"))
    })

    http.ListenAndServe(":8080", r)
}

Routing

Basic Routes

r := chi.NewRouter()

r.Get("/users", listUsers)
r.Get("/users/{id}", getUser)
r.Post("/users", createUser)
r.Put("/users/{id}", updateUser)
r.Delete("/users/{id}", deleteUser)

// Method not allowed handler
r.MethodNotAllowed(methodNotAllowedHandler)

// Not found handler
r.NotFound(notFoundHandler)

Path Parameters

r.Get("/users/{userID}", func(w http.ResponseWriter, r *http.Request) {
    userID := chi.URLParam(r, "userID")
    json.NewEncoder(w).Encode(map[string]string{"id": userID})
})

// Regex constraints
r.Get("/articles/{date:\\d{4}-\\d{2}-\\d{2}}", getArticleByDate)

// Catch-all
r.Get("/files/*", serveFiles)

Route Groups

r := chi.NewRouter()

r.Route("/api", func(r chi.Router) {
    r.Route("/v1", func(r chi.Router) {
        r.Get("/users", listUsersV1)
        r.Post("/users", createUserV1)
    })

    r.Route("/v2", func(r chi.Router) {
        r.Get("/users", listUsersV2)
        r.Post("/users", createUserV2)
    })
})

Subrouters

r := chi.NewRouter()

// Mount subrouter
apiRouter := chi.NewRouter()
apiRouter.Get("/users", listUsers)
apiRouter.Post("/users", createUser)

r.Mount("/api/v1", apiRouter)

Context

Request Context

func getUser(w http.ResponseWriter, r *http.Request) {
    // Path params
    userID := chi.URLParam(r, "userID")

    // Query params
    page := r.URL.Query().Get("page")

    // Headers
    auth := r.Header.Get("Authorization")

    // Context values
    user := r.Context().Value("user").(*User)

    json.NewEncoder(w).Encode(user)
}

Context with Values

func UserContext(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        userID := chi.URLParam(r, "userID")
        user, err := findUser(userID)
        if err != nil {
            http.Error(w, "User not found", http.StatusNotFound)
            return
        }

        ctx := context.WithValue(r.Context(), "user", user)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

r.Route("/users/{userID}", func(r chi.Router) {
    r.Use(UserContext)
    r.Get("/", getUser)
    r.Put("/", updateUser)
})

Middleware

Built-in Middleware

import "github.com/go-chi/chi/v5/middleware"

r := chi.NewRouter()

r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.Compress(5))
r.Use(middleware.Timeout(60 * time.Second))
r.Use(middleware.Throttle(100))
r.Use(middleware.Heartbeat("/ping"))

Custom Middleware

func TimingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        duration := time.Since(start)
        log.Printf("%s %s %v", r.Method, r.URL.Path, duration)
    })
}

r.Use(TimingMiddleware)

Authentication Middleware

func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        auth := r.Header.Get("Authorization")

        if auth == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }

        if !strings.HasPrefix(auth, "Bearer ") {
            http.Error(w, "Invalid token format", http.StatusUnauthorized)
            return
        }

        token := auth[7:]
        user, err := validateToken(token)
        if err != nil {
            http.Error(w, "Invalid token", http.StatusUnauthorized)
            return
        }

        ctx := context.WithValue(r.Context(), "user", user)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

r.Route("/api", func(r chi.Router) {
    r.Use(AuthMiddleware)
    r.Get("/me", getMe)
})

func getMe(w http.ResponseWriter, r *http.Request) {
    user := r.Context().Value("user").(*User)
    json.NewEncoder(w).Encode(user)
}

CORS Middleware

import "github.com/go-chi/cors"

r.Use(cors.Handler(cors.Options{
    AllowedOrigins:   []string{"https://example.com"},
    AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    AllowedHeaders:   []string{"Accept", "Authorization", "Content-Type"},
    AllowCredentials: true,
    MaxAge:           300,
}))

Response Helpers

render Package

import "github.com/go-chi/render"

type UserResponse struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

func (u *UserResponse) Render(w http.ResponseWriter, r *http.Request) error {
    return nil
}

func getUser(w http.ResponseWriter, r *http.Request) {
    user := &UserResponse{
        ID:    "1",
        Name:  "Alice",
        Email: "alice@example.com",
    }
    render.JSON(w, r, user)
}

// Error response
type ErrResponse struct {
    Err            error  `json:"-"`
    HTTPStatusCode int    `json:"-"`
    StatusText     string `json:"status"`
    ErrorText      string `json:"error,omitempty"`
}

func (e *ErrResponse) Render(w http.ResponseWriter, r *http.Request) error {
    render.Status(r, e.HTTPStatusCode)
    return nil
}

func ErrNotFound(err error) render.Renderer {
    return &ErrResponse{
        Err:            err,
        HTTPStatusCode: http.StatusNotFound,
        StatusText:     "Resource not found",
        ErrorText:      err.Error(),
    }
}

Manual JSON Response

func respond(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func getUser(w http.ResponseWriter, r *http.Request) {
    user := User{ID: "1", Name: "Alice"}
    respond(w, http.StatusOK, user)
}

Error Handling

type APIError struct {
    StatusCode int    `json:"-"`
    Message    string `json:"message"`
    Details    string `json:"details,omitempty"`
}

func (e *APIError) Error() string {
    return e.Message
}

func errorHandler(w http.ResponseWriter, r *http.Request, err error) {
    var apiErr *APIError
    if errors.As(err, &apiErr) {
        respond(w, apiErr.StatusCode, apiErr)
        return
    }

    respond(w, http.StatusInternalServerError, map[string]string{
        "message": "Internal server error",
    })
}

func getUser(w http.ResponseWriter, r *http.Request) {
    userID := chi.URLParam(r, "userID")
    user, err := findUser(userID)
    if err != nil {
        errorHandler(w, r, &APIError{
            StatusCode: http.StatusNotFound,
            Message:    "User not found",
        })
        return
    }
    respond(w, http.StatusOK, user)
}

Patterns

Resource Pattern

type UsersResource struct {
    db *sql.DB
}

func (rs UsersResource) Routes() chi.Router {
    r := chi.NewRouter()

    r.Get("/", rs.List)
    r.Post("/", rs.Create)

    r.Route("/{id}", func(r chi.Router) {
        r.Use(rs.UserCtx)
        r.Get("/", rs.Get)
        r.Put("/", rs.Update)
        r.Delete("/", rs.Delete)
    })

    return r
}

func (rs UsersResource) UserCtx(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        id := chi.URLParam(r, "id")
        user, err := rs.db.FindUser(id)
        if err != nil {
            http.Error(w, "User not found", http.StatusNotFound)
            return
        }
        ctx := context.WithValue(r.Context(), "user", user)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Mount resource
r.Mount("/users", UsersResource{db: db}.Routes())

Production Readiness

Health Checks

r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
    respond(w, http.StatusOK, map[string]string{"status": "healthy"})
})

r.Get("/ready", func(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
    defer cancel()

    if err := db.PingContext(ctx); err != nil {
        respond(w, http.StatusServiceUnavailable, map[string]string{
            "status":   "not ready",
            "database": "disconnected",
        })
        return
    }

    respond(w, http.StatusOK, map[string]string{
        "status":   "ready",
        "database": "connected",
    })
})

Graceful Shutdown

func main() {
    r := chi.NewRouter()
    r.Get("/", handler)

    srv := &http.Server{
        Addr:    ":8080",
        Handler: r,
    }

    go func() {
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatal(err)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := srv.Shutdown(ctx); err != nil {
        log.Fatal(err)
    }
}

Checklist

  • [ ] Recoverer middleware enabled
  • [ ] CORS configured
  • [ ] Request timeout set
  • [ ] Throttling enabled
  • [ ] Health/readiness endpoints
  • [ ] Graceful shutdown
  • [ ] Structured logging
  • [ ] Context-based values

When NOT to Use This Skill

  • Gin projects - Gin has more built-in features
  • Echo projects - Echo has different context patterns
  • Fiber projects - Fiber is Express-like and faster
  • Need built-in validation - Chi is minimal, use Gin or Echo
  • WebSocket-heavy apps - Other frameworks have better support

Anti-Patterns

| Anti-Pattern | Why It's Bad | Solution | |--------------|--------------|----------| | Not using chi.URLParam() | Manual param extraction | Use chi.URLParam(r, "id") | | Missing middleware order | Wrong execution sequence | Order middleware carefully | | Not using context for values | Global state issues | Use context.WithValue() | | Forgetting to call next.ServeHTTP() | Handler chain breaks | Always call next.ServeHTTP(w, r) | | No structured error handling | Inconsistent responses | Create helper functions | | Using subrouters incorrectly | Routes don't match | Use .Route() or .Mount() properly |

Quick Troubleshooting

| Problem | Diagnosis | Fix | |---------|-----------|-----| | Route not matching | Wrong path pattern | Check regex constraints and params | | Middleware not executing | Order issue | Place middleware before routes | | Context value is nil | Not set properly | Use context.WithValue() in middleware | | CORS errors | Not configured | Use cors.Handler() from go-chi/cors | | Panic on missing param | No default | Check chi.URLParam() return value | | 405 Method Not Allowed | Wrong HTTP method | Verify route method matches request |

Reference Documentation