Go HTTP 框架选型:标准库就够了,你纠结的是伪命题
我从 Go 1.0 开始写。十年间经历了 net/http 从「勉强能用」到「大部分场景够了」的转变。
2012 年你用标准库写 REST API,每个路由都要手写正则匹配,路径参数要自己从 URL 里抠,方法判断要写 if r.Method == "GET"。那时候第三方框架不是选择,是必需品。
2026 年的今天,Go 1.22 的路由增强已经出来两年了。很多人还在惯性选框架,没停下来看一眼标准库现在长什么样。
这篇不劝你不用框架,而是算一笔账——加一个框架的真正代价是什么,你买到了什么,你付出的值不值。
Go 1.22 改了什么
2024 年 2 月发布的 Go 1.22 给 net/http 带来了两个关键增强:
1. 方法匹配
mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
mux.HandleFunc("PUT /api/users/{id}", updateUser)
mux.HandleFunc("DELETE /api/users/{id}", deleteUser)
以前你得写 mux.HandleFunc("/api/users", handler),然后在 handler 里自己判断 r.Method,自己从路径里提取 ID。
2. 路径参数
mux.HandleFunc("GET /api/users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
// 直接取到 "id" 的值,不需要 chi.URLParam() 或 gin.Param()
})
注意写法是 {id} 不是 :id 也不是 :id。这是 Go 团队自己造的模式语法——支持通配符 {pathname} 和正则 {id:[0-9]+}。
这两条合起来,标准库的 ServeMux 从一个路由器残废变成了一个能用的路由器。
对比代码:同样的 CRUD,三种写法
写一个简单的用户 CRUD API,三个版本对比。
标准库(Go 1.22+)
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"sync"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
var (
users = make(map[int]User)
nextID = 1
mu sync.Mutex
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
mux.HandleFunc("GET /api/users/{id}", getUser)
mux.HandleFunc("PUT /api/users/{id}", updateUser)
mux.HandleFunc("DELETE /api/users/{id}", deleteUser)
log.Fatal(http.ListenAndServe(":8080", mux))
}
func listUsers(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
list := make([]User, 0, len(users))
for _, u := range users {
list = append(list, u)
}
json.NewEncoder(w).Encode(list)
}
func createUser(w http.ResponseWriter, r *http.Request) {
var u User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
http.Error(w, err.Error(), 400)
return
}
mu.Lock()
u.ID = nextID
nextID++
users[u.ID] = u
mu.Unlock()
w.WriteHeader(201)
json.NewEncoder(w).Encode(u)
}
func getUser(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(r.PathValue("id"))
mu.Lock()
u, ok := users[id]
mu.Unlock()
if !ok {
http.Error(w, "not found", 404)
return
}
json.NewEncoder(w).Encode(u)
}
func updateUser(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(r.PathValue("id"))
var u User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
http.Error(w, err.Error(), 400)
return
}
mu.Lock()
if _, ok := users[id]; !ok {
mu.Unlock()
http.Error(w, "not found", 404)
return
}
u.ID = id
users[id] = u
mu.Unlock()
json.NewEncoder(w).Encode(u)
}
func deleteUser(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(r.PathValue("id"))
mu.Lock()
delete(users, id)
mu.Unlock()
w.WriteHeader(204)
}
零依赖。go build 出来一个二进制。不香?
Chi
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"sync"
"github.com/go-chi/chi/v5"
)
// 结构体代码同上
func main() {
r := chi.NewRouter()
r.Get("/api/users", listUsers)
r.Post("/api/users", createUser)
r.Get("/api/users/{id}", getUser)
r.Put("/api/users/{id}", updateUser)
r.Delete("/api/users/{id}", deleteUser)
log.Fatal(http.ListenAndServe(":8080", r))
}
// handler 代码基本一样,只是 r.PathValue("id") 换成 chi.URLParam(r, "id")
Chi 的写法更干净一点——.Get() .Post() 链式调用比 mux.HandleFunc("GET ...", ...) 少了一个字符。但本质差异不值一提。
Gin
package main
import (
"net/http"
"strconv"
"sync"
"github.com/gin-gonic/gin"
)
// 结构体同上
func main() {
r := gin.Default()
r.GET("/api/users", listUsers)
r.POST("/api/users", createUser)
r.GET("/api/users/:id", getUser)
r.PUT("/api/users/:id", updateUser)
r.DELETE("/api/users/:id", deleteUser)
r.Run(":8080")
}
// Gin 用 c.Param("id") 取路径参数
// 返回值用 c.JSON() 封装
Gin 的标记是 :id 而不是 {id}。它多了 gin.Default()(自带 Logger + Recovery 中间件),c.JSON(),c.Param()。
看完了,说结论:对 90% 的 CRUD 项目,这三者的开发体验差异,不值得一个 import。
你为框架付出了什么
| 维度 | 标准库 | Chi | Gin |
|---|---|---|---|
| 依赖数 | 0 | 4 (chi + 3个间接) | 28+ |
| 编译时间 | 基准 | +4-8% | +15-30% |
| 二进制体积 | 基准 | +0.5MB | +3-8MB |
| CVE 暴露面 | 0(Go 官方跟踪) | 需监控 Chi 的 CVE | 需监控 Gin + 28 个间接依赖 |
| 学习曲线 | 0(你已经在用标准库) | 低(chi.URLParam) | 中(Gin Context 是独立的 API) |
| 升级风险 | 无(跟着 Go 版本走) | 低 | 中(Gin 大版本有 break change) |
依赖数不是无意义的信仰问题。 每多一个依赖,你就多一个需要跟踪的安全公告源、多一个 Go module graph 的复杂度、多一个 go mod tidy 上如果维护者发版不规范可能引入的问题。
你获得的东西值不值这个代价?
对大多数项目,不值。
什么时候该用框架
上面我说了"90% 的 CRUD 项目不需要框架"。剩下的 10% 是什么?
-
复杂中间件链。标准库的 ServeMux 没有中间件机制。你需要自己写一个包装函数把
http.Handler包一下。一个中间件两个函数还能忍,五个以上就难看了。Chi 的r.Use()链式中间件管理确实干净。 -
子路由/路由分组。
/api/v1/和/api/v2/分开处理,标准库的做法是声明多个 ServeMux 然后组合。Chi 的r.Route("/api/v1", func(r chi.Router) { ... })更直观。 -
需要极致的请求生命周期控制。Gin 的
c.Set()/c.Get()在请求范围内传递上下文比标准库的context.WithValue少写几行。但说实话,标准库的r.Context()配合context.WithValue一样能解决。 -
团队已经熟悉某个框架且项目已经有大量代码基于它。这时候强行迁移到标准库是不划算的——框架不是性能瓶颈,CI 时间也不是。
中间的选项
如果你需要中间件管理但不想引入 Gin 这种重型框架,Chi 是最优解:它的贡献是 chi.Router 接口和中间件链管理,路由本身已经和标准库的 ServeMux 趋同。它不改变你的 handler 签名(仍然是 http.Handler 不是 gin.HandlerFunc),这意味着你未来切换到标准库几乎没有成本。
实际上我对项目推荐演化路径是这个:
标准库 (1-5 个 handler) → Chi (5-20 个 handler + 中间件) → 标准库 (架构稳定后重构回来)
没错,做完后考虑重构回标准库。因为 Chi 的价值主要在开发期,部署后它只是一个中间层——你付的依赖税是无意义的。
收尾:选型不是选最强的
Go HTTP 框架选型这块,最大的认知误区是把框架当竞争力。
你的业务逻辑才是竞争力。你用什么路由库,你的用户不关心,你的 API 客户端不关心,你的简历可能在面试时被多看一眼但仅此而已。
Go 1.22 之后的标准库路由,已经不是"是否够用"的问题,而是"你到底需要什么"的问题。如果你说不上来你需要框架的哪个特性,那你就不需要框架。
用标准库写一个 HTTP 服务,go build 出来单个二进制,scp 到服务器,跑起来。没有 go.mod 的 indirect 依赖,没有 vendor 目录,没有 replace 指令。
这种轻盈本身,就是一种竞争力。
Go 1.22 是 2024 年 2 月发布的,我假设你已经在用 >= Go 1.22。如果你还在用 Go 1.21 及以下,标准库路由确实不够用,请用 Chi。然后升级 Go。
Go HTTP Framework Selection: The Standard Library Is Enough
I've been writing Go since 1.0. Over the last decade, net/http went from "barely usable" to "enough for most cases."
In 2012, writing a REST API with the standard library meant writing regex patterns by hand, extracting path parameters manually, and checking r.Method in every handler. Third-party frameworks weren't a choice — they were a necessity.
In 2026, Go 1.22's routing enhancements have been out for two years. Yet many developers still reach for a framework out of habit, without checking what the standard library now offers.
This article won't tell you to never use a framework. It will run the numbers: what a framework actually costs you, what you're buying, and whether it's worth the price.
What Changed in Go 1.22
Go 1.22 (released February 2024) added two key enhancements:
1. Method matching:
mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
mux.HandleFunc("PUT /api/users/{id}", updateUser)
2. Path parameters:
mux.HandleFunc("GET /api/users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
})
These two changes transformed ServeMux from a routing cripple to a usable router.
Code Comparison: Same CRUD, Three Ways
Standard Library (Go 1.22+)
mux := http.NewServeMux()
mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
mux.HandleFunc("GET /api/users/{id}", getUser)
mux.HandleFunc("PUT /api/users/{id}", updateUser)
mux.HandleFunc("DELETE /api/users/{id}", deleteUser)
http.ListenAndServe(":8080", mux)
Zero dependencies. One go build produces a single binary.
Chi
r := chi.NewRouter()
r.Get("/api/users", listUsers)
r.Post("/api/users", createUser)
r.Get("/api/users/{id}", getUser)
r.Put("/api/users/{id}", updateUser)
r.Delete("/api/users/{id}", deleteUser)
http.ListenAndServe(":8080", r)
Chi's method-chaining syntax is marginally cleaner. But the functional difference is negligible.
Gin
r := gin.Default()
r.GET("/api/users", listUsers)
r.POST("/api/users", createUser)
r.GET("/api/users/:id", getUser)
r.PUT("/api/users/:id", updateUser)
r.DELETE("/api/users/:id", deleteUser)
r.Run(":8080")
Gin uses :id instead of {id} and adds c.JSON(), c.Param(), and built-in middleware.
Bottom line: for 90% of CRUD projects, the development experience difference between these three is not worth a single import.
What You Pay for a Framework
| Dimension | stdlib | Chi | Gin |
|---|---|---|---|
| Dependencies | 0 | 4 (chi + 3 transitive) | 28+ |
| Build time | baseline | +4-8% | +15-30% |
| Binary size | baseline | +0.5MB | +3-8MB |
| CVE surface | 0 (Go team tracks) | must monitor Chi CVEs | must monitor Gin + 28 transitive deps |
| Learning curve | 0 (you already use stdlib) | Low | Medium (Gin Context is its own API) |
| Upgrade risk | None (follows Go version) | Low | Medium (Gin major versions break) |
Dependency count isn't an abstract concern. Each dependency adds a CVE source to track, a node in your module graph, and a potential go mod tidy headache when maintainers don't follow semver.
Is what you gain worth this cost?
For most projects: no.
When You Actually Need a Framework
The 10% of cases where a framework makes sense:
-
Complex middleware chains. ServeMux has no built-in middleware mechanism. You can write wrapper functions for 1-2 middlewares, but beyond 5, Chi's
r.Use()chain is genuinely cleaner. -
Sub-routing / route groups.
/api/v1/vs/api/v2/— stdlib requires multiple ServeMux instances. Chi'sr.Route("/api/v1", func(r chi.Router) { ... })is more readable. -
Request-scoped context (heavy use). Gin's
c.Set()/c.Get()is a few lines shorter than stdlib'scontext.WithValue. Neither is wrong. -
Team already invested. If your codebase uses Gin heavily, migrating to stdlib for purity is a waste of money. A framework isn't your bottleneck.
The Middle Option
Chi is the best middle ground: it adds middleware chain management without changing your handler signatures (still http.Handler). Migrating back to stdlib costs almost nothing.
My recommended evolution path:
stdlib (1-5 handlers) → Chi (5-20 handlers + middleware) → stdlib (refactor back after stabilization)
Build with Chi, then refactor back to stdlib when the architecture is stable. Chi's value is in development — in production it's just a tax.
Close: Selection Isn't About the Strongest
The biggest misconception in Go HTTP framework selection is treating the framework as a competitive advantage.
Your business logic is the competitive advantage. Your users don't care what router you use. Your API clients don't care. Your resume might get a second glance in an interview — that's it.
Since Go 1.22, the standard library's routing isn't a question of "is it enough." It's a question of "what do you actually need." If you can't name the framework feature you need, you don't need the framework.
Write your HTTP service with stdlib. go build a single binary. scp it to your server. Run it. No indirect dependencies. No vendor directory. No replace directives.
That lightness is itself a competitive advantage.
Go 1.22 was released in Feb 2024. If you're still on Go ≤1.21, stdlib routing is genuinely insufficient — use Chi. And upgrade your Go.