# 第 20 课:HTTP 客户端与简单服务 ## 学习目标 学会区分客户端、服务端、请求、响应与状态码,将上一课的 JSON 用在网络通信中。先读 [main.go](./main.go),运行成功后再看 [main_test.go](./main_test.go)。本课只使用标准库,不安装第三方依赖。 本例**不访问外部网站**:程序自己在本机回环地址启动临时 HTTP 服务,自动选择可用端口,客户端发起一次请求,函数返回时关闭服务。地址每次可能不同;如果系统禁止本地监听,运行和部分测试可能失败。 ## 1. 请求与响应 ```text 客户端 -- HTTP 请求 --> 服务端 客户端 <-- HTTP 响应 -- 服务端 ``` 请求包含方法、路径、查询参数等;响应包含状态码、响应头和响应体。示例请求相当于: ```text GET /hello?name=小朱 ``` 实际传输时,中文、空格和特殊字符会进行 URL 编码。 | 状态码 | 本课含义 | | --- | --- | | 200 | 请求成功,响应体是 JSON | | 400 | 姓名缺失或全是空白 | | 405 | 请求方法不是 GET | HTTP 错误状态与网络 error 是两回事:服务端返回 400 时,客户端可能已经成功收到 HTTP 响应,`client.Get` 的 err 仍可能是 nil,因此还需要检查 StatusCode。 ## 2. 服务端处理函数 ```go func greetingHandler(w http.ResponseWriter, r *http.Request) { name := r.URL.Query().Get("name") // 检查方法和参数后写回 JSON。 } ``` - w:用于写响应状态、响应头和响应体的接口。 - r:请求指针,可读取 Method 和 URL 等信息。 - `Query().Get("name")`:获取第一个同名查询参数;找不到时得到空字符串。 - `http.Error`:写入指定状态码和纯文本错误响应。 - `json.NewEncoder(w).Encode(...)`:直接将 Go 值编码到响应体,通常会追加换行。 应先设置响应头,再写状态和正文。本例写正文时默认状态是 200。响应一旦开始写入,后续编码出错时不一定还能更改状态码,因此示例只记录写入错误。 `http.NewServeMux` 是路由器;`HandleFunc("/hello", greetingHandler)` 将路径交给处理函数。这里使用普通路径注册,方法检查在处理函数里完成。 ## 3. 客户端请求 ```go params := url.Values{} params.Set("name", name) response, err := client.Get(baseURL + "/hello?" + params.Encode()) ``` url.Values 负责正确编码查询参数,不要手工拼接未经编码的用户输入。 请求成功取得 response 后,应关闭 `response.Body`;本例使用 defer。请求失败时 response 可能不存在,不能在检查错误之前就 defer 关闭。 示例的 `http.Client` 设置三秒 Timeout,避免无限等待。服务端返回非 200 时,函数返回一个包含状态码的错误;本课为简洁没有读取错误响应体。 成功响应通过 `io.LimitReader` 限制读取,再检查是否超过 64 KiB,避免无限读取。之后用 `json.Unmarshal` 解析。只解析成功不代表业务内容一定有效,真实项目还要验证字段、内容类型等。 非成功状态下直接关闭未完全读取的响应体,可能影响连接复用;它不会让这个示例错误地继续解析失败响应,真实客户端需结合连接复用、错误体大小上限和协议设计处理。 ## 4. 为什么这里使用 httptest ```go server := httptest.NewServer(mux) defer server.Close() ``` httptest 是标准库中的测试辅助工具。本例借它在本机快速启动临时服务,简化启动、选端口和关闭流程,方便一次运行看完整请求链路。 它适合教学和测试,**不是生产服务器的部署方式**。真实服务通常使用 `http.Server` 配置地址、超时等并调用 ListenAndServe,还要考虑优雅关闭、日志、身份认证和 HTTPS。本课不会在公网暴露服务。 本例 HTTP 明文且只在本机回环地址演示,不要用于传输敏感信息或当作生产安全配置。 ## 5. 自动测试 - `httptest.NewRequest` + `NewRecorder`:不经过真实网络,直接调用处理函数,检查状态和内容。 - `httptest.NewServer`:启动临时本地服务,测试完整的客户端与服务端交互。 - 测试覆盖有效请求、空姓名、错误方法、中文和特殊字符、非法 JSON、响应过大。 这种测试不依赖外部网站,但 NewServer 仍需要本机网络监听权限。测试服务通过 defer 关闭。 ## 编译并运行 在**仓库根目录**执行: ```powershell New-Item -ItemType Directory -Force bin/20_http | Out-Null go build -o ./bin/20_http/http_demo.exe ./lessons/20_http .\bin\20_http\http_demo.exe ``` 修改源码后需要重新编译。也可快速编译并运行: ```powershell go run ./lessons/20_http ``` 预期输出(地址和端口每次可能不同): ```text 本地临时服务: http://127.0.0.1:随机端口 客户端收到: 你好,小朱! 演示结束,临时服务即将关闭 ``` 运行本课测试: ```powershell go test -count=1 -v ./lessons/20_http ``` ## 动手练习 - [ ] 把客户端姓名改成自己的名字,观察返回内容。 - [ ] 姓名中加入空格、中文和 `&`,理解 URL 编码的作用。 - [ ] 在测试中用 POST 请求,确认返回 405。 - [ ] 为返回数据增加一个字段,同步修改结构体、服务端和测试。 - [ ] 增加一个请求失败时的测试;不要依赖公网网站。 - [ ] 将客户端响应大小上限调小,验证超限错误,再恢复。 如果要另行手敲,放到本课的 practice 子目录,用 `go run ./lessons/20_http/practice` 运行,不要在同一个包里重复声明 main 或同名函数。 下一课学习 goroutine 并发基础,先理解如何启动任务和等待任务结束。 [上一课:命令行输入](../19_cli/README.md) · [学习总览](../../README.md)