1、http客户端

Go语言的net/http包不仅提供了HTTP服务器功能,还包含了功能强大的HTTP客户端API,让我们能够轻松地与各种Web服务进行交互。

简单请求

Get

package main

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

func main() {
	resp, err := http.Get("https://www.baidu.com/")
	if err != nil {
		fmt.Println(err)
	}
	defer resp.Body.Close()
	// 响应码
	fmt.Println(resp.Status)
	// 响应头
	fmt.Println(resp.Header)
	// 响应体
	result, _ := io.ReadAll(resp.Body)
	fmt.Println(string(result))
}

Post

package main

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

func main() {
	data := map[string]any{
		"name": "lucy",
		"age":  18,
	}
	j, _ := json.Marshal(data)
	resp, _ := http.Post("http://localhost:8080/test", "application/json", bytes.NewReader(j))
	bs, _ := io.ReadAll(resp.Body)
	fmt.Println(string(bs))
}

其他方法

使用net/http包来发送http请求,支持的方法有:

const (
    MethodGet     = "GET"
    MethodHead    = "HEAD"
    MethodPost    = "POST"
    MethodPut     = "PUT"
    MethodPatch   = "PATCH" // RFC 5789
    MethodDelete  = "DELETE"
    MethodConnect = "CONNECT"
    MethodOptions = "OPTIONS"
    MethodTrace   = "TRACE"
)

示例:

// POST请求
resp, err := http.Post("https://httpbin.org/post", "application/json", 
                       strings.NewReader(`{"name":"gopher","message":"hello"}`))

// PUT请求
req, err := http.NewRequest(http.MethodPut, "https://httpbin.org/put", 
                           strings.NewReader(`{"name":"gopher","status":"updated"}`))
client := &http.Client{}
resp, err := client.Do(req)

// DELETE请求
req, err := http.NewRequest(http.MethodDelete, "https://httpbin.org/delete?id=123", nil)
resp, err := client.Do(req)

请求头与请求参数

设置请求头和URL参数是常见需求:

// 创建请求
req, err := http.NewRequest("GET", "https://api.example.com/users", nil)
if err != nil {
    log.Fatal(err)
}

// 添加请求头
req.Header.Add("Authorization", "Bearer token123456")
req.Header.Add("Content-Type", "application/json")
req.Header.Add("User-Agent", "GoHTTPClient/1.0")

// 添加URL参数
q := req.URL.Query()
q.Add("page", "1")
q.Add("limit", "10")
q.Add("sort", "created_at")
req.URL.RawQuery = q.Encode()

// 发送请求
client := &http.Client{}
resp, err := client.Do(req)
// ...处理响应

处理HTTP响应

读取响应体

处理HTTP响应的核心是正确读取响应体。前面我们已经看到了使用io.ReadAll的方式,但还有其他模式:

// 分块读取大型响应
resp, err := http.Get("https://example.com/large-file")
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

// 使用缓冲读取
buf := make([]byte, 1024)
for {
    n, err := resp.Body.Read(buf)
    if err == io.EOF {
        break // 读取完成
    }
    if err != nil {
        log.Fatal(err)
    }
    
    // 处理读取的部分数据
    fmt.Printf("读取了 %d 字节的数据\n", n)
    fmt.Printf("数据片段: %s\n", buf[:n])
}

对于结构化数据(如JSON),可以直接解码:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
)

func main() {
	resp, err := http.Get("https://api.github.com/users/golang")
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	var user struct {
		Login  string `json:"login"`
		ID     int    `json:"id"`
		Name   string `json:"name"`
		Bio    string `json:"bio"`
		Public struct {
			Repos int `json:"public_repos"`
		} `json:"public_repos"`
	}

	if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("用户信息: %+v\n", user)
}

处理状态码

resp, err := http.Get("https://example.com/api/resource")
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

switch resp.StatusCode {
case http.StatusOK, http.StatusCreated, http.StatusAccepted:
    // 请求成功
    fmt.Println("请求成功!")
    // 处理响应...
    
case http.StatusNotFound:
    fmt.Println("资源不存在")
    
case http.StatusUnauthorized, http.StatusForbidden:
    fmt.Println("认证或授权失败")
    
case http.StatusInternalServerError:
    fmt.Println("服务器错误")
    
default:
    fmt.Printf("未预期的状态码: %d\n", resp.StatusCode)
}

处理响应头

resp, err := http.Get("https://example.com")
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

// 获取特定响应头
contentType := resp.Header.Get("Content-Type")
fmt.Printf("内容类型: %s\n", contentType)

// 遍历所有响应头
fmt.Println("所有响应头:")
for name, values := range resp.Header {
    for _, value := range values {
        fmt.Printf("%s: %s\n", name, value)
    }
}

// 检查响应是否支持压缩
if resp.Header.Get("Content-Encoding") == "gzip" {
    // 处理gzip压缩内容
    reader, err := gzip.NewReader(resp.Body)
    if err != nil {
        log.Fatal(err)
    }
    defer reader.Close()
    // 从解压缩的reader读取内容
    body, err := io.ReadAll(reader)
    // ...
}

高级HTTP客户端配置

默认客户端

当你需要自定义http请求而无需自定义客户端时,可以使用http包提供的默认客户端来实现。

client := &http.Client{}

// 使用自定义客户端发送请求
resp, err := client.Get("https://example.com")
// ...

自定义HTTP客户端

client := &http.Client{
    // 设置超时时间
    Timeout: 10 * time.Second,
    
    // 自定义重定向策略
    CheckRedirect: func(req *http.Request, via []*http.Request) error {
        // 限制最多重定向3次
        if len(via) >= 3 {
            return fmt.Errorf("停止重定向:已达到最大重定向次数")
        }
        return nil
    },
}

// 使用自定义客户端发送请求
resp, err := client.Get("https://example.com")
// ...

传输层配置(Transport)

transport := &http.Transport{
    // 代理设置
    Proxy: http.ProxyURL(&url.URL{
        Scheme: "http",
        Host:   "proxy.example.com:8080",
    }),
    
    // TLS配置
    TLSClientConfig: &tls.Config{
        InsecureSkipVerify: false, // 生产环境请设为false
        MinVersion:         tls.VersionTLS12,
    },
    
    // 连接池设置
    MaxIdleConns:        100,
    MaxIdleConnsPerHost: 10,
    IdleConnTimeout:     90 * time.Second,
    
    // 拨号设置
    DialContext: (&net.Dialer{
        Timeout:   30 * time.Second,
        KeepAlive: 30 * time.Second,
    }).DialContext,
    
    // HTTP/2支持
    ForceAttemptHTTP2: true,
}

client := &http.Client{
    Transport: transport,
    Timeout:   15 * time.Second,
}

// 使用配置后的客户端
resp, err := client.Get("https://example.com")
// ...

请求上下文(Context)

使用context可以实现请求取消和超时控制:

import (
    "context"
    "net/http"
    "time"
)

func main() {
    // 创建一个5秒超时的上下文
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel() // 确保取消函数被调用
    
    // 创建带上下文的请求
    req, err := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/data", nil)
    if err != nil {
        log.Fatal(err)
    }
    
    // 发送请求
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        // 检查是否因超时而失败
        if ctx.Err() == context.DeadlineExceeded {
            log.Println("请求超时")
        } else {
            log.Printf("请求失败: %v", err)
        }
        return
    }
    defer resp.Body.Close()
    
    // 处理响应
    // ...
}

高级HTTP请求

提交表单数据

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
)

func main() {
	// 创建表单数据
	formData := url.Values{
		"username": {"gopher"},
		"email":    {"gopher@example.com"},
		"message":  {"Hello from Go!"},
	}

	// 发送表单
	resp, err := http.PostForm("https://httpbin.org/post", formData)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	// 处理响应
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("响应: %s\n", body)
}

multipart/form-data和文件上传

上传文件需要使用multipart/form-data格式:

package main

import (
	"bytes"
	"io"
	"log"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
)

func main() {
	// 打开要上传的文件
	file, err := os.Open("example.jpg")
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	// 创建multipart写入器
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)

	// 创建文件表单字段
	part, err := writer.CreateFormFile("file", filepath.Base(file.Name()))
	if err != nil {
		log.Fatal(err)
	}

	// 复制文件内容到表单字段
	if _, err = io.Copy(part, file); err != nil {
		log.Fatal(err)
	}

	// 添加额外的文本字段
	writer.WriteField("description", "示例图片上传")
	writer.WriteField("category", "测试")

	// 完成写入
	writer.Close()

	// 创建请求
	req, err := http.NewRequest("POST", "https://httpbin.org/post", body)
	if err != nil {
		log.Fatal(err)
	}

	// 设置Content-Type头,指定boundary
	req.Header.Set("Content-Type", writer.FormDataContentType())

	// 发送请求
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	// 处理响应
	// ...
}

处理cookies

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"net/http/cookiejar"
)

func main() {
	// 创建HTTP客户端,启用cookie jar
	jar, err := cookiejar.New(nil)
	if err != nil {
		log.Fatal(err)
	}

	client := &http.Client{
		Jar: jar,
	}

	// 发送第一个请求,服务器可能会设置cookie
	resp, err := client.Get("https://httpbin.org/cookies/set?name=value")
	if err != nil {
		log.Fatal(err)
	}
	resp.Body.Close() // 关闭但不处理响应

	// 发送第二个请求,Jar自动附带上一个响应的cookie
	resp, err = client.Get("https://httpbin.org/cookies")
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	// 读取响应,应当显示服务器收到的cookie
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("响应: %s\n", body)

	// 手动添加cookie到请求
	req, err := http.NewRequest("GET", "https://httpbin.org/cookies", nil)
	if err != nil {
		log.Fatal(err)
	}
	req.AddCookie(&http.Cookie{
		Name:  "custom_cookie",
		Value: "custom_value",
	})
	resp, err = client.Do(req)
	// ...
}

构建可重用的HTTP客户端

对于重复调用同一API,创建专用客户端结构是最佳实践:

// APIClient 封装API调用功能
type APIClient struct {
    BaseURL    string
    HTTPClient *http.Client
    Token      string
}

// NewAPIClient 创建新的API客户端
func NewAPIClient(baseURL, token string) *APIClient {
    return &APIClient{
        BaseURL: baseURL,
        HTTPClient: &http.Client{
            Timeout: 10 * time.Second,
        },
        Token: token,
    }
}

// Get 发送GET请求到指定路径
func (c *APIClient) Get(path string, params map[string]string) ([]byte, error) {
    // 构建完整URL
    u, err := url.Parse(c.BaseURL)
    if err != nil {
        return nil, err
    }
    u.Path = path
    
    // 添加查询参数
    q := u.Query()
    for key, value := range params {
        q.Add(key, value)
    }
    u.RawQuery = q.Encode()
    
    // 创建请求
    req, err := http.NewRequest("GET", u.String(), nil)
    if err != nil {
        return nil, err
    }
    
    // 添加认证头
    req.Header.Add("Authorization", "Bearer "+c.Token)
    req.Header.Add("Content-Type", "application/json")
    
    // 发送请求
    resp, err := c.HTTPClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    // 检查状态码
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        return nil, fmt.Errorf("API错误: %d %s", resp.StatusCode, resp.Status)
    }
    
    // 读取并返回响应体
    return io.ReadAll(resp.Body)
}

// Post 发送POST请求到指定路径
func (c *APIClient) Post(path string, data interface{}) ([]byte, error) {
    // 将数据编码为JSON
    jsonData, err := json.Marshal(data)
    if err != nil {
        return nil, err
    }
    
    // 创建请求
    u, err := url.Parse(c.BaseURL)
    if err != nil {
        return nil, err
    }
    u.Path = path
    
    req, err := http.NewRequest("POST", u.String(), bytes.NewBuffer(jsonData))
    if err != nil {
        return nil, err
    }
    
    // 添加头
    req.Header.Add("Authorization", "Bearer "+c.Token)
    req.Header.Add("Content-Type", "application/json")
    
    // 发送请求
    resp, err := c.HTTPClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    // 检查状态码
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        return nil, fmt.Errorf("API错误: %d %s", resp.StatusCode, resp.Status)
    }
    
    // 读取并返回响应体
    return io.ReadAll(resp.Body)
}

性能优化与最佳实践

连接池与客户端重用

Go的http.Client内部使用连接池机制来提高性能:

一些关键的性能优化点:

  1. 避免为每个请求创建新的http.Client
  2. 配置适当的空闲连接数量和超时时间
  3. 确保始终关闭响应体,即使不读取它
// 创建一个全局的HTTP客户端,整个应用程序共享
var client = &http.Client{
    Timeout: 10 * time.Second,
    Transport: &http.Transport{
        MaxIdleConns:        100,              // 连接池中的最大空闲连接数
        MaxIdleConnsPerHost: 10,               // 每个主机的最大空闲连接数
        IdleConnTimeout:     90 * time.Second, // 空闲连接超时时间
    },
}

// 在应用中重复使用此客户端
func fetchURL(url string) ([]byte, error) {
    resp, err := client.Get(url)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    return io.ReadAll(resp.Body)
}

请求超时设置

client := &http.Client{
    // 设置整体请求超时(包括连接、重定向和读取响应体)
    Timeout: 10 * time.Second,
    Transport: &http.Transport{
        // 仅连接建立的超时
        DialContext: (&net.Dialer{
            Timeout: 5 * time.Second,
        }).DialContext,
        // TLS握手超时
        TLSHandshakeTimeout: 5 * time.Second,
        // 响应头超时
        ResponseHeaderTimeout: 5 * time.Second,
        // 空闲连接超时
        IdleConnTimeout: 90 * time.Second,
    },
}

并发请求处理

func fetchConcurrent(urls []string) []Result {
    ch := make(chan Result, len(urls))
    var wg sync.WaitGroup
    
    // 限制最大并发数
    semaphore := make(chan struct{}, 10)
    
    for _, url := range urls {
        wg.Add(1)
        go func(url string) {
            defer wg.Done()
            
            // 获取信号量
            semaphore <- struct{}{}
            defer func() { <-semaphore }()
            
            // 发送请求
            resp, err := http.Get(url)
            if err != nil {
                ch <- Result{URL: url, Error: err}
                return
            }
            defer resp.Body.Close()
            
            // 读取响应
            body, err := io.ReadAll(resp.Body)
            if err != nil {
                ch <- Result{URL: url, Error: err}
                return
            }
            
            ch <- Result{URL: url, Body: body}
        }(url)
    }
    
    // 等待所有请求完成并关闭通道
    go func() {
        wg.Wait()
        close(ch)
    }()
    
    // 收集结果
    var results []Result
    for result := range ch {
        results = append(results, result)
    }
    
    return results
}

type Result struct {
    URL   string
    Body  []byte
    Error error
}

HTTP/2支持

Go的net/http包自动支持HTTP/2,但可以通过配置优化

client := &http.Client{
    Transport: &http.Transport{
        // 启用HTTP/2
        ForceAttemptHTTP2: true,
        // HTTP/2连接设置
        MaxIdleConnsPerHost: 100,
    },
}

响应体流式处理

// 下载大文件
func downloadLargeFile(url, destPath string) error {
    // 创建目标文件
    out, err := os.Create(destPath)
    if err != nil {
        return err
    }
    defer out.Close()
    
    // 发送GET请求
    resp, err := http.Get(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    
    // 检查状态码
    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("下载失败,状态码: %d", resp.StatusCode)
    }
    
    // 创建进度条(可选)
    fileSize := resp.ContentLength
    progress := 0
    counter := &WriteCounter{
        Total: fileSize,
    }
    
    // 使用io.Copy将响应体直接流式复制到文件
    _, err = io.Copy(out, io.TeeReader(resp.Body, counter))
    if err != nil {
        return err
    }
    
    return nil
}

// WriteCounter 用于跟踪下载进度
type WriteCounter struct {
    Total     int64
    Downloaded int64
}

func (wc *WriteCounter) Write(p []byte) (int, error) {
    n := len(p)
    wc.Downloaded += int64(n)
    wc.PrintProgress()
    return n, nil
}

func (wc *WriteCounter) PrintProgress() {
    if wc.Total <= 0 {
        fmt.Printf("\r下载中: %d bytes", wc.Downloaded)
        return
    }
    
    percentage := float64(wc.Downloaded) / float64(wc.Total) * 100
    fmt.Printf("\r下载进度: %.2f%% (%d/%d bytes)", percentage, wc.Downloaded, wc.Total)
}

上传文件

当你需要上传文件时,可以通过multipart.Writer来实现。

func main() {
    filePath := "go.mod"
    multipartWriter, reqBody, err := newUploadFile(filePath)
    if err != nil {
        fmt.Println(err)
        return
    }

    req, err := http.NewRequest(http.MethodPost, "http://localhost:8080/file", reqBody)
    if err != nil {
        fmt.Println("create request failed:", err)
        return
    }
    req.Header.Set("Content-Type", multipartWriter.FormDataContentType())
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        fmt.Println("send request failed:", err)
        return
    }
    defer resp.Body.Close()

    respBody, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("read response failed:", err)
        return
    }
    fmt.Println("upload file succeed:", string(respBody))
}

func newUploadFile(filePath string) (*multipart.Writer, *bytes.Buffer, error) {
    var reqBody bytes.Buffer
    multipartWriter := multipart.NewWriter(&reqBody)
    part, err := multipartWriter.CreateFormFile("upload", filepath.Base(filePath))
    if err != nil {
        return nil, nil, fmt.Errorf("create form file failed: %v", err)
    }

    file, err := os.Open(filePath)
    if err != nil {
        return nil, nil, fmt.Errorf("open file failed: %v", err)
    }
    defer file.Close()

    _, err = io.Copy(part, file)
    if err != nil {
        return nil, nil, fmt.Errorf("copy file content failed: %v", err)
    }

    err = multipartWriter.Close()
    if err != nil {
        return nil, nil, fmt.Errorf("close multipart writer failed: %v", err)
    }
    return multipartWriter, &reqBody, nil
}

下载文件

通过http接口下载文件的实现特别简单,将响应体数据写入本地文件即可。

下载文件到download目录下,从响应头中的Content-Disposition字段提取文件名:

func main() {
    resp, err := http.Get("http://localhost:8080/file")
    if err != nil {
        fmt.Println("http get failed:", err)
        return
    }
    defer resp.Body.Close()
    respBody, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("read response body failed:", err)
        return
    }
    filename := filenameFromRespHeader(resp.Header)
    err = os.WriteFile(path.Join("download", filename), respBody, os.ModePerm)
    if err != nil {
        return
    }
    fmt.Println("http get succeed:", string(respBody))
}

func filenameFromRespHeader(header http.Header) string {
    r := regexp.MustCompile(`^.*filename="(.*)"`)
    s := header.Get("Content-Disposition")
    ss := r.FindStringSubmatch(s)
    if len(ss) >= 2 {
        return ss[1]
    }
    return "unknown"
}