标准库路由也能写得很舒服
很长一段时间里,很多人写 Go Web 服务会直接选择 Gin、Echo、Chi 这类框架。它们当然好用,但标准库 net/http 也一直在进步。现代 Go 的 ServeMux 已经能表达更清楚的路由模式,包括 HTTP 方法和路径参数。对于小型 JSON API、内部工具和学习项目,标准库完全可以胜任。
学习标准库路由的价值在于,你能看清 HTTP handler 的本质:请求进入路由器,匹配到处理函数,处理函数解析输入、调用业务逻辑、写出响应。框架只是把这些步骤包装得更丰富。
这篇文章用一个文章 API 做例子,讲如何组织 ServeMux 路由和 handler。
最小路由
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "ok")
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
log.Fatal(server.ListenAndServe())
路由模式里可以带方法,比如 GET /healthz。这样你不需要在 handler 里手动判断方法:
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
标准库会帮你处理不匹配的方法。对简单 API 来说,这让 handler 更专注业务。
路径参数
注册:
mux.HandleFunc("GET /articles/{id}", handler.getArticle)
读取参数:
func (h *Handler) getArticle(w http.ResponseWriter, r *http.Request) {
rawID := r.PathValue("id")
id, err := strconv.ParseInt(rawID, 10, 64)
if err != nil || id <= 0 {
writeError(w, http.StatusBadRequest, "invalid article id")
return
}
article, err := h.store.Get(r.Context(), id)
if err != nil {
writeError(w, http.StatusNotFound, "article not found")
return
}
writeJSON(w, http.StatusOK, article)
}
r.PathValue("id") 会读取 {id} 捕获的部分。以前标准库路由没有这类能力,很多人会自己拆字符串或引入路由库。现在小项目里可以先用标准库。
Handler 持有依赖
type Handler struct {
store *ArticleStore
}
func NewHandler(store *ArticleStore) *Handler {
return &Handler{store: store}
}
注册路由:
func (h *Handler) Routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", h.health)
mux.HandleFunc("GET /articles", h.listArticles)
mux.HandleFunc("POST /articles", h.createArticle)
mux.HandleFunc("GET /articles/{id}", h.getArticle)
return mux
}
main 只负责组装:
store := NewArticleStore()
handler := NewHandler(store)
server := &http.Server{
Addr: ":8080",
Handler: handler.Routes(),
}
这种结构比在 main 里散落注册函数更清楚,也方便测试 handler。
JSON 响应辅助函数
func writeJSON(w http.ResponseWriter, status int, value interface{}) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(value); err != nil {
log.Printf("encode response: %v", err)
}
}
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]string{
"error": message,
})
}
创建文章:
type createArticleRequest struct {
Title string `json:"title"`
Content string `json:"content"`
}
func (h *Handler) createArticle(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
var req createArticleRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid json")
return
}
article, err := h.store.Create(r.Context(), req.Title, req.Content)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusCreated, article)
}
路由已经限制了 POST,所以 handler 不再关心方法。
什么时候仍然需要框架
标准库适合小服务和清楚 API,但框架仍然有价值。比如你需要成熟中间件生态、参数绑定、OpenAPI 集成、统一验证、复杂路由组、认证插件、团队已有框架规范,使用框架很合理。
学习标准库不是为了排斥框架,而是为了理解框架背后的基本模型。你知道 handler、middleware、routing 如何工作后,再用框架会更稳,也更容易排查问题。
小结
现代 Go 的 ServeMux 已经能表达方法和路径参数,小型 API 完全可以先用标准库实现。用 Routes 方法集中注册路由,用 Handler 结构体持有依赖,用辅助函数统一 JSON 响应,代码会非常清楚。
标准库路由的优势是依赖少、行为直接、容易测试。等项目复杂度真的需要框架时,再引入也不迟。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。