REST 客户端

net/http 提供的 http.Client 是调用第三方 API 的标准工具。生产代码应始终使用共享的 Client实例(内置连接池复用 TCP/TLS 连接),并显式配置超时。

基本请求

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "time"
)

func main() {
    // 共享 Client:全局复用连接池,务必设置超时
    client := &http.Client{
        Timeout: 10 * time.Second, // 覆盖连接、发送、响应全过程
    }

    resp, err := client.Get("https://httpbin.org/get")
    if err != nil {
        fmt.Println("请求失败:", err)
        return
    }
    defer resp.Body.Close() // 必须关闭,否则连接无法复用

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("读取响应失败:", err)
        return
    }
    fmt.Println("状态码:", resp.StatusCode)
    fmt.Printf("响应前 100 字节: %.100s\n", body)
}
Warning

两个最常见错误:

  1. 忘记 defer resp.Body.Close():连接无法回到连接池,高并发下耗尽文件描述符。
  2. 未设置 Client.Timeout:默认零值表示永不超时,一个慢端点即可拖垮调用方。

即使不读响应体也应完整读取并关闭,否则连接同样无法复用:io.Copy(io.Discard, resp.Body)

发送 JSON 与自定义请求

POST、PUT 等需要构造 http.Request

package main

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "time"
)

type CreateOrderReq struct {
    Item  string  `json:"item"`
    Price float64 `json:"price"`
}

type CreateOrderResp struct {
    OrderID string `json:"order_id"`
}

func createOrder(ctx context.Context, client *http.Client, req CreateOrderReq) (*CreateOrderResp, error) {
    payload, err := json.Marshal(req)
    if err != nil {
        return nil, err
    }

    r, err := http.NewRequestWithContext(ctx, http.MethodPost,
        "https://api.example.com/orders", bytes.NewReader(payload))
    if err != nil {
        return nil, err
    }
    r.Header.Set("Content-Type", "application/json")
    r.Header.Set("Authorization", "Bearer "+os.Getenv("API_TOKEN")) // 敏感信息走环境变量

    resp, err := client.Do(r)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        body, _ := io.ReadAll(resp.Body)
        return nil, fmt.Errorf("非预期状态码 %d: %s", resp.StatusCode, body)
    }

    var out CreateOrderResp
    if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
        return nil, err
    }
    return &out, nil
}

func main() {
    client := &http.Client{Timeout: 5 * time.Second}
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    order, err := createOrder(ctx, client, CreateOrderReq{Item: "keyboard", Price: 299})
    if err != nil {
        fmt.Println("下单失败:", err)
        return
    }
    fmt.Println("订单号:", order.OrderID)
}

示例需 import "os"

查询参数与重试

package main

import (
    "fmt"
    "net/http"
    "net/url"
    "time"
)

func search(client *http.Client, keyword string) error {
    // url.Values 自动处理转义与编码
    q := url.Values{}
    q.Set("q", keyword)
    q.Set("page", "1")

    resp, err := client.Get("https://httpbin.org/get?" + q.Encode())
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    fmt.Println(resp.StatusCode)
    return nil
}

func main() {
    client := &http.Client{Timeout: 10 * time.Second}
    _ = search(client, "golang tutorial")
}

对幂等 GET 请求可增加简单重试(指数退避):

func getWithRetry(client *http.Client, url string, attempts int) (*http.Response, error) {
    var lastErr error
    for i := 0; i < attempts; i++ {
        resp, err := client.Get(url)
        if err == nil && resp.StatusCode < 500 {
            return resp, nil // 成功或非服务端错误:直接返回
        }
        if resp != nil {
            resp.Body.Close()
        }
        lastErr = err
        time.Sleep(time.Duration(1<<i) * 100 * time.Millisecond) // 100ms、200ms、400ms...
    }
    return nil, lastErr
}

并发请求

结合并发编程章节的能力,可并行调用多个接口:

package main

import (
    "fmt"
    "io"
    "net/http"
    "sync"
    "time"
)

func fetchAll(urls []string) map[string]int {
    client := &http.Client{Timeout: 5 * time.Second}
    var mu sync.Mutex
    status := make(map[string]int, len(urls))
    var wg sync.WaitGroup

    for _, u := range urls {
        wg.Add(1)
        go func(link string) {
            defer wg.Done()
            resp, err := client.Get(link)
            if err != nil {
                return
            }
            defer resp.Body.Close()
            _, _ = io.Copy(io.Discard, resp.Body)

            mu.Lock() // map 并发写需要加锁
            status[link] = resp.StatusCode
            mu.Unlock()
        }(u)
    }
    wg.Wait()
    return status
}

func main() {
    urls := []string{
        "https://httpbin.org/status/200",
        "https://httpbin.org/status/404",
    }
    for u, code := range fetchAll(urls) {
        fmt.Println(u, code)
    }
}

小结

  • Client 必须共享且必须设置超时;响应体必须读取并关闭。
  • 自定义请求配合 NewRequestWithContext 实现取消与超时传递。
  • 并发请求遵循 goroutine + 锁/通道的标准模式。