Go ServeMux 路由入门:用标准库写更清楚的 HTTP 路由

本文讲解现代 Go 标准库 ServeMux 的路由写法、方法匹配、路径参数、Handler 结构和 JSON API 示例。

标准库路由也能写得很舒服

很长一段时间里,很多人写 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 响应,代码会非常清楚。

标准库路由的优势是依赖少、行为直接、容易测试。等项目复杂度真的需要框架时,再引入也不迟。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页