跳转到内容

chi — Go 标准库友好的轻量 HTTP router

是什么

chi 是一个用 Go 写的小型 HTTP 路由器(router):你给它一张 “URL 模式 → 处理函数” 的表,它负责把进来的请求送到对的函数。日常类比:像写字楼前台——访客说要找 7 楼的张三,前台查表后告诉他坐哪部电梯、走哪条走廊。chi 就是 Go 服务里的这个前台。

它最大的卖点是和 Go 标准库 net/http 100% 兼容:你写的处理函数仍然是标准的 func(w http.ResponseWriter, r *http.Request),所以社区任何一个标准 net/http 中间件都能直接拿来用。

r := chi.NewRouter()
r.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id") // 从 URL 取参数
fmt.Fprintf(w, "user %s", id)
})
http.ListenAndServe(":8080", r)

为什么重要

不理解 chi 的角色,下面这些事都说不清:

  • 为什么 Go 写 REST 服务可以不用任何重型框架也能写得清爽——chi 几乎是 net/http 的天然补丁
  • 为什么大量 Go 开源项目(API gateway / SaaS 后端)都引 chi 而不是 gin——它没有自创 handler 签名
  • 为什么”中间件” 在 Go 里看起来这么像洋葱——chi 的 Use 就是一层一层把 handler 包起来
  • 为什么同样写 REST API,Go 比 Python 启动快 10 倍以上——背后是 chi 这种”零依赖 + 直接走 net/http” 的设计

核心要点

chi 的设计可以拆成 三件事

  1. 路由匹配用 Radix 树:把所有 URL 模式存成一棵”按字符前缀分支” 的树,匹配时一边走树一边比字符。类比:像查英文字典——先找首字母 c,再找 ch,再找 chi。比一个一个 if 串快得多。

  2. 中间件栈像洋葱r.Use(logger) 会把后续每个 handler 都用 logger 包一层;r.With(auth).Get(...) 只对这条路由加一层。类比:洋葱外层先碰到请求,剥到最里才是真正的业务函数。

  3. URL 参数走 context{id} 这种占位符匹配后,chi 把值塞进 r.Context(),handler 用 chi.URLParam(r, "id") 取。这避免了改请求对象本身,符合 Go 1.7+ 的 context 习惯。

实践案例

案例 1:最小 REST API

package main
import ("net/http"; "github.com/go-chi/chi/v5")
func main() {
r := chi.NewRouter()
r.Get("/ping", func(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte("pong"))
})
http.ListenAndServe(":8080", r)
}

逐部分解释chi.NewRouter() 造一个空路由表;r.Get("/ping", ...) 注册一条 “GET /ping → 返回 pong” 的规则;http.ListenAndServe 是 Go 标准库的,证明 chi 能直接当 http.Handler 用。

案例 2:叠中间件栈

r := chi.NewRouter()
r.Use(middleware.Logger) // 每个请求打日志
r.Use(middleware.Recoverer) // 兜住 panic 不让进程崩
r.Get("/", home) // 受上面两层保护

逐部分解释Use 注册的中间件会按顺序包裹之后注册的所有 handler。请求进来时,先过 Logger(外层),再过 Recoverer,最后到 home;返回时反向往外走。

注意 r.Use(...) 的位置:写在 r.Get(...) 之前才生效;写在后面,那条路由不会被该中间件保护——这是 chi 最常踩的坑,下一节会展开。

案例 3:嵌套子路由

r.Route("/api/v1/users", func(r chi.Router) {
r.Get("/", listUsers) // GET /api/v1/users
r.Post("/", createUser)
r.Route("/{id}", func(r chi.Router) {
r.Get("/", getUser) // GET /api/v1/users/42
r.Get("/posts", userPosts) // GET /api/v1/users/42/posts
})
})

逐部分解释Route 创建一个共享前缀的子组,里面注册的所有路由都自动加上 /api/v1/users 前缀,且共享父路由的中间件——典型的 RESTful 资源嵌套写法。

踩过的坑

  1. 中间件顺序非常敏感r.Use(auth) 必须写在 r.Get(...) 之前,才能保护那条路由——chi 的中间件只对之后注册的路由生效。常见 bug:把 Use 写在末尾,结果一条都没保护到。

  2. URL 参数在路由前取不到:如果在全局中间件(router 级 Use)里调 chi.URLParam(r, "id"),会拿到空字符串——因为路由还没匹配,参数还没塞进 context。要拿参数得在 handler 或路由级中间件里。

  3. r.WithContext(ctx) 返回新对象不是改原来的:很多人写 r.WithContext(ctx) 然后还用旧的 r,等于白干。正确写法是 r = r.WithContext(ctx) 再往下传,或直接 next.ServeHTTP(w, r.WithContext(ctx))

  4. MountRoute 不一样Route 是把子路由”内联”进来,共享父中间件栈;Mount 是把另一个独立 router 挂上去,绕开父中间件。要让子模块继承父 auth 中间件的话,用 Route 别用 Mount。

适用 vs 不适用场景

适用

  • 用 Go 写 REST API / 微服务,想保留 net/http 兼容性的项目
  • 中等复杂度路由(几十到几百条)+ 多层中间件栈的服务
  • 团队希望换 router 不重写业务 handler 的工程(chi 用标准签名,迁移成本低)

不适用

  • 极致追求 throughput 的网关层 → 用 fasthttp 系列(gin / fiber 底层),但要放弃 net/http 生态
  • 需要框架级”全家桶”(ORM / DI / config 模板都打包好)→ 选 express 风格的全家桶(在 Go 里类似 kratos / go-kit)
  • 单文件脚本式工具,路由就 1-2 条 → 直接用 http.HandleFunc 就够,引 chi 反而过度设计

历史小故事(可跳过)

  • 2015 年:Peter Kieltyka 在做内部 API 服务时发现 net/http 的 ServeMux 太弱、gin 又太”自成一派”,于是抽出一个 idiomatic 的 router 开源。
  • 2017 年:Go 1.7 把 context 进了标准库,chi 借势把 URL 参数全面迁到 context,奠定”零依赖 + 标准签名” 的基调。
  • 2020 年前后:社区 benchmark 表明 chi 在简单参数场景做到 ~384 ns/op,与 gin 同档却没引入私有 handler 签名。
  • 2023 年起:弃用早期的 RealIP 中间件(容易被伪造 X-Forwarded-For 欺骗),改推 ClientIPFrom* 系列,强调安全默认值。
  • 当下:作为 v5 主线版本被广泛集成进 Go 生态,是大量 OpenAPI / GraphQL / gRPC-Gateway 项目默认的 HTTP 路由层。

学到什么

  1. 路由器本质是”模式匹配 + 派发”——把这一步独立出来,业务 handler 才能保持纯净
  2. 兼容标准接口胜过创新签名——chi 因为不发明 handler 类型,吃到了 net/http 整个生态的中间件红利
  3. 中间件栈是嵌套包装,不是回调队列,理解了”洋葱模型” 才不会写错顺序
  4. Radix 树作为前缀匹配的经典数据结构,在 router、文件系统、IP 路由表里反复出现,值得专门学一次

延伸阅读

关联

  • express —— Node.js 的轻量 router + 中间件框架,chi 的设计哲学和它一脉相承
  • fastapi —— Python 用类型注解写 API,chi 选择了另一条路:极简 + 标准接口
  • flask —— Python 的轻量 web 框架,“装饰器注册路由” vs chi 的 method 调用注册可对照
  • django —— Python 全家桶框架,chi 故意走相反方向(不打包 ORM / template)
  • http-2 —— HTTP/2 是 chi 这类 router 的下层协议,决定 keep-alive / multiplexing 行为
  • tcp —— router 之下还有 TCP 连接管理,理解 socket 才能完整理解请求生命周期

反向链接

  • actix-web —— Actix Web — Rust 上长期占据 TechEmpower 榜首的 web 框架
  • django —— Django — 全功能 batteries-included 的 Python web 框架
  • encore —— Encore — 类型安全 Go/TS 后端框架,基础设施即代码
  • express —— Express — Node.js 最经典的 Web 框架
  • fastapi —— FastAPI — 用 Python 类型注解写 API
  • flask —— Flask — 用装饰器把 URL 接到函数上的 Python 微框架
  • http-2 —— HTTP/2 — 把 HTTP 从文本协议改造成二进制多路复用
  • kratos —— kratos — Go 微服务一锅出 HTTP 和 gRPC 两份服务
  • poem —— poem — 一份 impl 块同时变 HTTP API + OpenAPI 文档站的 Rust 框架
  • rocket —— Rocket — 用 Rust attribute macro 把路由当函数签名写的 web 框架
  • tcp —— TCP — 在不可靠的 IP 上凿出一条 reliable 字节流