深色模式
Go HTTP 客户端
概述
Go 标准库里的 net/http 已经够用很久了。大多数业务代码并不缺“能发请求”的方法,真正容易出问题的是细节:超时没设、响应体没关、状态码没判断、请求头和请求体拼得七零八落。
所以这篇文章不再按 Get、Post、PostForm 零散罗列,而是先给出一套更稳的基本写法,再说明哪些简写方法适合什么场景。
先用 http.Client 和 http.NewRequest
日常代码里,最稳的基本组合通常是:
http.Client负责发送请求http.NewRequest或http.NewRequestWithContext负责构造请求
例如,一个带查询参数和请求头的 GET 请求:
go
package main
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
func main() {
values := url.Values{}
values.Set("q", "golang")
values.Set("page", "1")
client := &http.Client{
Timeout: 5 * time.Second,
}
req, err := http.NewRequestWithContext(
context.Background(),
http.MethodGet,
"https://example.com/search?"+values.Encode(),
nil,
)
if err != nil {
panic(err)
}
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
panic(fmt.Errorf("unexpected status: %s", resp.Status))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Println(string(body))
}这里真正值得养成习惯的是这几步:
- 客户端要有超时。
- 请求尽量带
context。 - 响应体读完前先
defer resp.Body.Close()。 - 不要假设请求成功,先检查
StatusCode。
发送 JSON 请求的常见写法
业务接口里更常见的是 POST JSON。可以直接用 json.Marshal 把请求体编码后送出去:
go
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
)
type CreateUserRequest struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
payload := CreateUserRequest{
Name: "Tom",
Age: 18,
}
data, err := json.Marshal(payload)
if err != nil {
panic(err)
}
client := &http.Client{Timeout: 5 * time.Second}
req, err := http.NewRequest(http.MethodPost, "https://example.com/users", bytes.NewReader(data))
if err != nil {
panic(err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer <token>")
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
panic(fmt.Errorf("create user failed: %s", resp.Status))
}
}重点不在 json.Marshal 本身,而在于保持请求构造清晰:方法、地址、请求体、请求头分开写,排错时会省很多时间。
响应如果是 JSON,直接解码到结构体
如果服务端返回 JSON,通常不必先 ReadAll 再二次解析,直接用 json.NewDecoder 就够了:
go
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
var user User
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return err
}这比先读成 []byte 再 json.Unmarshal 更直接,也更适合大多数接口响应处理。
Get、Post、PostForm 什么时候还能用
标准库也提供了几个简写方法:
http.Gethttp.Posthttp.PostForm
它们能用,但边界很明显:
- 写 demo 或一次性脚本时很方便。
- 请求很简单、也不需要复杂控制时可以用。
- 一旦涉及超时、认证头、上下文、复用客户端,还是应该回到
Client + Request。
特别是 http.PostForm,它适合提交 application/x-www-form-urlencoded 表单,但如果项目主要是 JSON API,它出现的频率通常没有想象中高。
生产代码里最常见的四个细节
不要每发一次请求都新建一个客户端
http.Client 可以复用。长期运行的服务里,通常会把它做成共享实例。每次请求都新建一个客户端,不仅啰嗦,也不利于连接复用。
一定要设超时
没有超时的 HTTP 调用,线上迟早会咬人。哪怕先只设一个总超时,也比完全不设强。
别忘了检查状态码
网络层没有报错,不等于业务成功。500、404、401 都会正常返回一个 resp。
不再推荐继续依赖 ioutil
旧代码里常见 ioutil.ReadAll,现在直接用 io.ReadAll 就行,标准库已经把这部分迁到 io 和 os 里了。
