G05第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

HTTP 服务与路由

HTTP 服务与路由 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

这课用标准库 net/http 把书签服务挂上端口:健康检查、列表、详情三个接口,处理方法、路径参数、请求头和状态码,不引入任何 Web 框架。

问题场景

前端想知道书签服务活没活着、有哪些书签、单个长什么样。标准库从 Go 1.22 起路由能力就够用了:方法加路径直接写进模式串,路径参数一行取出。框架迟早要接触,但先弄明白「一个 HTTP 请求在标准库里怎么走」,之后看框架只是看一层糖。

核心解释

路由表用 ServeMux,Go 1.22 的模式语法支持方法和通配符:

func newMux(store *MemoryStore) http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("ok"))
	})
	mux.HandleFunc("GET /bookmarks", func(w http.ResponseWriter, r *http.Request) {
		writeJSON(w, http.StatusOK, store.List())
	})
	mux.HandleFunc("GET /bookmarks/{id}", func(w http.ResponseWriter, r *http.Request) {
		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
		if err != nil {
			http.Error(w, "invalid id", http.StatusBadRequest)
			return
		}
		b, err := store.Get(id)
		if errors.Is(err, ErrNotFound) {
			http.Error(w, "bookmark not found", http.StatusNotFound)
			return
		}
		if err != nil {
			http.Error(w, "internal error", http.StatusInternalServerError)
			return
		}
		writeJSON(w, http.StatusOK, b)
	})
	return mux
}

func writeJSON(w http.ResponseWriter, code int, v any) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(code)
	json.NewEncoder(w).Encode(v)
}

func main() {
	store := NewMemoryStore()
	store.Add(Bookmark{Title: "Go 官网", URL: "https://go.dev", Tags: []string{"go"}})
	log.Fatal(http.ListenAndServe(":8080", newMux(store)))
}

四件事值得盯紧:模式串 GET /bookmarks/{id} 同时声明了方法和路径,其他方法打进来自动得到 405 Method Not Allowed,不用手写;r.PathValue("id") 按大括号里的名字取路径段;响应头必须在 WriteHeader 之前 Set,写了状态码再改头会被静默丢弃;JSON 编码统一收进 writeJSON,Content-Type 只写一次。

把路由注册提成 newMux(store) 函数不是随手为之:G07 的中间件要包在这层外面,G13 的测试要直接调它,现在提成函数是给后面两课铺路。ListenAndServe 的第二个参数就是 Handler 接口——ServeMux 实现了它,中间件以后也在这层包。log.Fatal 兜住启动失败(比如端口被占),进程退出码非 0,脚本里能立刻发现。

路由匹配还有两条默认规则值得知道:精确模式优先于通配符,GET /bookmarks/new 和 GET /bookmarks/{id} 同时注册时,new 请求走精确那条,不会被当成 id=new;没有任何模式匹配的路径由 mux 统一回 404,不用自己写兜底。

示例拆解

go run .

curl -i http://127.0.0.1:8080/healthz
# HTTP/1.1 200 OK,body: ok

curl -i http://127.0.0.1:8080/bookmarks/1
# HTTP/1.1 200 OK,body: {"id":1,...}

curl -i http://127.0.0.1:8080/bookmarks/abc
# HTTP/1.1 400 Bad Request,invalid id

curl -i -X POST http://127.0.0.1:8080/bookmarks/1
# HTTP/1.1 405 Method Not Allowed

curl -i http://127.0.0.1:8080/bookmarks/999
# HTTP/1.1 404 Not Found

五种响应对应五类真实情况:健康、正常、参数错、方法错、不存在。逐个 curl 一遍,比看十遍代码记得牢——每个状态码在代码里都有对应的那一行。顺手再发一个 GET /bookmarks/new,观察它落在 404 而不是 id 解析错误:路由怎么分流,测过才有底。

动手任务

  1. 加 DELETE /bookmarks/{id}:成功 204,不存在 404(给 MemoryStore 补 Delete)。
  2. 给列表加过滤:GET /bookmarks?tag=go,用 r.URL.Query().Get("tag"),为空则不过滤。
  3. 给 writeJSON 加 X-Content-Type-Options: nosniff 头,用 curl -i 确认生效。

常见错误

  • 先 WriteHeader 再 Set Content-Type:头被忽略,浏览器可能把 JSON 当纯文本显示。
  • 忘记检查 ParseInt 的 err:解析失败拿到 0,把「无效 id」查成 0 号书签。
  • 用字符串拼接路径再手动 if/else 分发:路由一多就失控,交给 ServeMux 模式串。
  • healthz 里做重活(全表扫描):探活工具会高频调用,只做最轻的存活判断。

检查与复盘

完成下方检查题。自查清单:

  • 能不查资料写出方法+通配符的路由模式并取路径参数
  • 说得清 400/404/405/500 分别由谁触发
  • JSON 响应统一走 writeJSON,状态码之前设置响应头

参考资料

检查题共 1 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1单选简单

Go 1.22+ 的 net/http ServeMux 注册了 mux.HandleFunc("GET /books/{id}", detail) 与 mux.HandleFunc("POST /books", create)。用 DELETE 方法请求 /books/3,结果是?

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...