🔖 Go 语言学习教程 · 第 23 篇
📌 本系列基于 Go 1.23 编写,代码兼容 Go 1.21+
全文约 4500 字 | 阅读约 15 分钟
欢迎来到第二十三篇 👋
Go 的 net/http 标准库已经覆盖了构建 HTTP 服务所需的大部分基础能力:客户端、服务端、请求上下文、路由、超时和连接复用都可以从标准库开始。
本篇会搭建一个小型 HTTP 服务,学习请求和响应、路由匹配、客户端超时、中间件雏形,以及 HTTP/2 和优雅关闭的基本概念。
✅ 上一篇练习题答案
以下是上一篇练习题的参考答案,欢迎在评论区分享你的解法~
1. 带标签的用户响应
go
type UserResponse struct {
ID int `json:"id"`
DisplayName string `json:"display_name"`
Password string `json:"-"`
}
调用 json.Marshal 后,密码字段不会出现在输出中。
2. 大整数精度实验
使用 json.Decoder 调用 UseNumber(),从 map[string]any 中取出 json.Number,再用 strconv.ParseInt 或 ParseUint 转换。不要先转成 float64。
3. omitempty 的限制
普通 int 的零值是 0,无法区分"没有传"和"明确传入 0"。需要这种区分时,把字段声明为 *int,用 nil 表示缺失。
4. 流式 Decoder
循环调用 Decode,先判断 errors.Is(err, io.EOF),再处理其他错误;成功解码后立即处理事件,不必等整个输入读完。
🧐 一、HTTP 请求和响应
一个 HTTP 服务端处理函数接收 http.ResponseWriter 和 *http.Request:
go
func hello(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("Hello, Go"))
}
响应头要在第一次写状态码或响应体之前设置。调用 Write 时,如果没有显式调用 WriteHeader,服务器通常会自动写入 200。
JSON 响应可以统一封装:
go
func writeJSON(w http.ResponseWriter, status int, value any) error {
data, err := json.Marshal(value)
if err != nil {
return err
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_, err = w.Write(data)
return err
}
真实项目还要统一错误响应格式,避免一部分接口返回字符串,另一部分返回对象。
📖 二、使用 ServeMux 路由
可以使用标准库的 ServeMux 注册处理器:
go
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", health)
mux.HandleFunc("GET /users/{id}", getUser)
mux.HandleFunc("POST /users", createUser)
在 Go 1.22 及以上,模式可以包含 HTTP 方法和路径通配符。处理函数中可以读取路径参数:
go
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
_ = writeJSON(w, http.StatusOK, map[string]string{"id": id})
}
如果项目需要兼容更早版本,可以使用第三方路由器或手动解析路径,但要明确版本边界。路由层应该负责匹配和参数提取,不要把数据库查询、业务规则都写进处理函数。
方法和路径优先级
更具体的路径通常优先于更宽泛的路径。注册路由时,应避免互相重叠导致维护者难以判断最终匹配结果。
🌐 三、HTTP 客户端
最简单的 GET:
go
resp, err := http.Get("https://example.com")
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return err
}
fmt.Println(resp.StatusCode, string(body))
生产环境不建议直接使用没有超时策略的默认客户端。可以创建带超时的 http.Client:
go
client := &http.Client{
Timeout: 5 * time.Second,
}
需要携带请求体、请求头或 Context 时,使用 http.NewRequestWithContext:
go
payload := strings.NewReader(`{"name":"小明"}`)
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
"https://example.com/users",
payload,
)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
响应体必须关闭,否则连接可能无法复用,长期运行会造成资源耗尽。
💡 四、服务端超时
服务端也需要配置超时:
go
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
不同超时保护不同阶段:
| 配置 | 作用 |
|---|---|
ReadHeaderTimeout |
等待请求头,减少慢速请求占用 |
ReadTimeout |
读取完整请求的最长时间 |
WriteTimeout |
写响应的最长时间 |
IdleTimeout |
Keep-Alive 空闲连接时间 |
上传大文件、长轮询和流式响应可能需要单独设计,不能盲目套用同一个超时值。
🧩 五、中间件模式
中间件本质是接收一个 Handler,再返回一个增强后的 Handler:
go
type Middleware func(http.Handler) http.Handler
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
fmt.Printf("%s %s %s\n", r.Method, r.URL.Path, time.Since(start))
})
}
组合中间件:
go
handler := logging(mux)
server := &http.Server{
Addr: ":8080",
Handler: handler,
}
中间件可以用于日志、鉴权、恢复 panic、请求 ID 和限流。注意中间件顺序会影响行为,例如恢复中间件通常应包在最外层,才能捕获内部处理器的 panic。
🔒 六、请求体、状态码与输入校验
接收 JSON 时,限制请求体大小:
go
limited := http.MaxBytesReader(w, r.Body, 1<<20)
defer r.Body.Close()
decoder := json.NewDecoder(limited)
decoder.DisallowUnknownFields()
var request CreateUserRequest
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
状态码要表达清晰的语义:
| 状态码 | 常见场景 |
|---|---|
| 200 | 查询或更新成功 |
| 201 | 创建资源成功 |
| 204 | 成功但无响应体 |
| 400 | 请求格式或参数错误 |
| 401 | 缺少或无效身份 |
| 403 | 已识别但没有权限 |
| 404 | 资源不存在 |
| 500 | 服务端未预期错误 |
不要把所有错误都返回 200 再在 JSON 中写一个 code。HTTP 状态码本身就是客户端和监控系统可以理解的协议。
🛑 七、优雅关闭
服务停止时,应该停止接收新请求,并等待已有请求完成:
go
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
go func() {
if err := server.ListenAndServe(); err != nil &&
!errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Println("关闭服务失败:", err)
}
Shutdown 需要和部署平台的停止信号配合。超时后仍未结束的请求会被强制中断,因此业务代码也要正确响应请求 Context。
🚀 八、HTTP/2 基础
Go 的 HTTPS 服务通常可以协商使用 HTTP/2。HTTP/2 支持多路复用、头部压缩和流优先级等能力,多个请求可以共享一个连接。
应用层代码通常仍使用 net/http 的请求和响应模型,不需要为每条 HTTP/2 stream 手动写调度逻辑。需要注意的是:
- TLS 配置和证书部署会影响协商结果;
- 代理和负载均衡可能改变客户端到服务端的协议;
- 不要把 HTTP/2 的多路复用等同于业务层无限并发;
- 仍需配置请求限制、超时和连接数策略。
📝 九、本篇小结
| 要点 | 总结 |
|---|---|
| Handler | 接收请求并写出响应 |
| ServeMux | 负责路由匹配和路径参数 |
| HTTP Client | 配置超时,关闭响应体 |
| 服务端超时 | 防止慢请求长期占用资源 |
| 中间件 | 以 Handler 包装 Handler,复用横切逻辑 |
| 输入边界 | 限制请求体并校验 JSON |
| 优雅关闭 | 使用 Shutdown 等待请求完成 |
| HTTP/2 | 多路复用,但仍需业务限流和超时 |
💬 一句话总结:
一个可靠的 HTTP 服务不仅要能返回结果,还要处理路由、超时、输入大小、资源关闭、中间件和优雅退出。
✏️ 十、练习题
- 🖥️ 动手实践 :使用
ServeMux实现GET /healthz和GET /users/{id}。 - 🔧 客户端超时:实现一个带 Context 和 3 秒超时的 HTTP GET 函数。
- 🤔 思考题 :为什么
http.Response.Body必须关闭?不关闭会带来什么问题? - 📚 服务端实践:为一个 HTTP 服务增加日志中间件和优雅关闭逻辑。
💡 练习题答案会在下一篇中公布,欢迎在评论区分享你的答案~
关注我,一起学编程
如果本文对你有帮助,欢迎 点赞、在看、转发、关注。