Menu

Golang HTTP 客户端:GET、POST JSON、超时与请求头

如何用 net/http 在 Go 中发送 HTTP 请求:http.Get、读取并关闭响应体、检查状态码、http.Client 超时、带 context 和请求头的请求、查询参数,以及用 POST 发送 JSON。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

GET 请求

本页的每个示例都会用 httptest.NewServer 启动自己的服务器,并通过 localhost 上真实的 TCP 连接调用它。把 srv.URL 换成 https://api.example.com 这样的真实地址,客户端代码完全不用改。

这里体现了三条规则:

  1. 先检查 err。 它报告的是根本没拿到响应的失败:DNS 错误、连接被拒绝、超时、TLS 问题。
  2. 检查完错误后立即 defer resp.Body.Close()。 响应体会让连接保持打开。不关闭响应体就会泄漏连接。
  3. 读取或解码响应体。 文本用 io.ReadAll,JSON 用 json.NewDecoder(resp.Body).Decode(&v)。

状态码不是错误

只要服务器返回了响应,包括 404 和 500,http.Get 都会返回 err == nil。状态码需要你自己检查:

io.LimitReader 限制你从错误响应体中读取的数据量,这样行为异常的服务器就没法让你把几兆字节的内容塞进一条错误信息里。很多 API 用 201、202 或 204 表示成功;如果任何 2xx 都算成功,就检查 resp.StatusCode >= 200 && resp.StatusCode < 300。

超时:真实流量绝不要用默认客户端

http.Get、http.Post 和 http.DefaultClient 没有整体超时(默认的 transport 只限制建立连接和 TLS 握手的时间)。如果服务器接受了连接却一直不回应,你的 goroutine 就会永远阻塞。请创建你自己的客户端:

Client.Timeout 覆盖整个交互过程:建立连接、重定向以及读取响应体。它返回的错误是一个 *url.Error,其 Timeout() == true,errors.As 可以通过 net.Error 接口找到它。只创建一个客户端并在各处复用;http.Client 可以安全地被并发使用。连接池位于它的 Transport 中,而不在 Client 里:每个请求新建一个 http.Client{} 仍然共享 http.DefaultTransport 及其连接池,但每个请求新建一个 http.Transport,每次都会从一个空的连接池开始。

带 context 和请求头的请求

http.NewRequestWithContext 构建一个请求,你可以先对它做定制,再用 client.Do 发送。context 在客户端超时之外,又为单个请求提供了截止时间或取消能力;在服务器中,它还能把发出的调用与传入请求的生命周期绑定在一起。

把用户输入直接拼接进 URL("?q=" + q)遇到 &、# 和空格就会出错。url.Values 及其 Encode 方法会负责转义,并对键排序。路径片段则使用 url.PathEscape。

POST JSON

client.Post(url, contentType, body) 是发送带该 Content-Type 请求头的请求的快捷方式。其他情况(认证请求头、PUT、PATCH、DELETE)就用 http.NewRequestWithContext 指定方法,再调用 client.Do。提交表单时,client.PostForm(url, url.Values{...}) 会编码这些值并设置 application/x-www-form-urlencoded。

连接复用

只有当你把上一个响应体读到末尾并关闭它时,客户端才会复用这个 TCP 连接。如果你提前停止读取(比如遇到错误状态码之后),而又要向同一个主机发送很多请求,就在关闭前把它读空:

defer func() {
	io.Copy(io.Discard, resp.Body)
	resp.Body.Close()
}()

如果响应体可能非常大,就改为通过 io.LimitReader 读空,或者接受丢掉这个连接。

重定向

客户端会自动跟随最多 10 次重定向,resp.Request.URL 会告诉你最终到达了哪里。如果想在第一次重定向时就停下,比如为了读取 Location 响应头,就设置 CheckRedirect:

client := &http.Client{
	Timeout: 10 * time.Second,
	CheckRedirect: func(req *http.Request, via []*http.Request) error {
		return http.ErrUseLastResponse // return the 3xx response itself
	},
}

常见错误

  • 在生产环境中使用 http.Get 或 http.DefaultClient。 没有超时。请创建一个设置了 Timeout 的 http.Client。
  • 忘记 resp.Body.Close()。 连接会不断泄漏,直到请求开始失败。
  • 在检查 err 之前就关闭响应体。 当 err 不为 nil 时,resp 是 nil,resp.Body.Close() 会引发 panic。
  • 把 err == nil 当成成功。 要检查 resp.StatusCode。
  • 每个请求都新建一个 http.Transport。 每个 transport 都有自己的连接池,所以每个请求都会打开一个新连接(还要做一次新的 TLS 握手)。共用一个 transport,通常是通过一个共享的 http.Client。
  • 手动拼接查询字符串。 使用 url.Values。

常见问题

Go 语言怎么发送 GET 请求?

resp, err := http.Get(url),检查 err,然后 defer resp.Body.Close(),再用 io.ReadAll(resp.Body) 读取响应体,或用 json.NewDecoder(resp.Body).Decode(&v) 解码。还要检查 resp.StatusCode:404 或 500 并不是 http.Get 返回的错误。

Go 语言怎么给 HTTP 请求设置超时?

创建一个带超时的客户端 client := &http.Client{Timeout: 10 * time.Second},用它代替 http.Get 或 http.DefaultClient。后两者没有整体超时:如果服务器接受了连接却一直不回应,调用就会永远阻塞。要为单个请求设置时限,就用 http.NewRequestWithContext 构建请求,并传入 context.WithTimeout 创建的 context。

Go 语言怎么发送带 JSON 请求体的 POST 请求?

先把值编码成 JSON,再发送:body, _ := json.Marshal(v),然后 http.Post(url, "application/json", bytes.NewReader(body))。如果要添加 Authorization 之类的请求头,就用 http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) 构建请求,设置 req.Header.Set("Content-Type", "application/json"),再用 client.Do(req) 发送。

Go 里为什么必须关闭 resp.Body?

响应体是一个建立在已打开连接上的流。关闭它会把连接放回连接池以便复用;忘记关闭会泄漏连接和文件描述符,直到程序达到上限。即使不读取响应体也要关闭它;如果希望连接被复用,就要把它读到末尾。

Coddy 编程语言插图

和 Coddy 一起学编程

开始