Golang HTTP请求完全指南

引言

Golang 提供了强大而灵活的 HTTP 客户端和服务器实现,通过标准库 net/http 包可以轻松处理 HTTP 请求和响应。本文将详细介绍如何在 Go 中发起各种 HTTP 请求、处理响应以及一些高级用法和最佳实践。

目录

基础 HTTP 请求

GET 请求

最简单的 HTTP 请求是 GET 请求,用于从服务器获取数据:

package main

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

func main() {
	// 发起 GET 请求
	resp, err := http.Get("https://api.github.com/users/golang")
	if err != nil {
		log.Fatalf("请求失败: %v", err)
	}
	// 确保响应体被关闭
	defer resp.Body.Close()

	// 读取响应体
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatalf("读取响应失败: %v", err)
	}

	// 打印状态码和响应内容
	fmt.Printf("状态码: %d\n", resp.StatusCode)
	fmt.Printf("响应体: %s\n", body)
}

POST 请求

POST 请求用于向服务器提交数据:

package main

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

func main() {
	// 请求体数据
	data := strings.NewReader(`{"name":"gopher","message":"Hello, World!"}`)

	// 发起 POST 请求,指定 Content-Type 为 application/json
	resp, err := http.Post(
		"https://httpbin.org/post",
		"application/json",
		data,
	)
	if err != nil {
		log.Fatalf("请求失败: %v", err)
	}
	defer resp.Body.Close()

	// 读取响应体
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatalf("读取响应失败: %v", err)
	}

	fmt.Printf("状态码: %d\n", resp.StatusCode)
	fmt.Printf("响应体: %s\n", body)
}

POST 表单请求

对于表单提交,可以使用 PostForm 方法:

package main

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

func main() {
	// 创建表单数据
	formData := url.Values{
		"username": {"gopher"},
		"password": {"secret123"},
		"remember": {"true"},
	}

	// 发起 POST 表单请求
	resp, err := http.PostForm("https://httpbin.org/post", formData)
	if err != nil {
		log.Fatalf("请求失败: %v", err)
	}
	defer resp.Body.Close()

	// 读取响应体
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatalf("读取响应失败: %v", err)
	}

	fmt.Printf("状态码: %d\n", resp.StatusCode)
	fmt.Printf("响应体: %s\n", body)
}

自定义 HTTP 请求

创建自定义请求

对于更复杂的 HTTP 请求,可以使用 http.NewRequest 创建自定义请求:

package main

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

func main() {
	// 创建请求体
	data := strings.NewReader(`{"query":"golang"}`)

	// 创建自定义请求
	req, err := http.NewRequest("PUT", "https://httpbin.org/put", data)
	if err != nil {
		log.Fatalf("创建请求失败: %v", err)
	}

	// 使用默认客户端发送请求
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatalf("请求失败: %v", err)
	}
	defer resp.Body.Close()

	// 读取响应体
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatalf("读取响应失败: %v", err)
	}

	fmt.Printf("状态码: %d\n", resp.StatusCode)
	fmt.Printf("响应体: %s\n", body)
}

设置请求头

可以通过 Header 字段设置请求头:

// 设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer token123")
req.Header.Set("User-Agent", "Golang-HTTP-Client/1.0")

设置查询参数

设置 URL 查询参数:

// 创建请求
req, err := http.NewRequest("GET", "https://api.example.com/search", nil)
if err != nil {
	log.Fatalf("创建请求失败: %v", err)
}

// 获取查询参数对象并添加参数
q := req.URL.Query()
q.Add("q", "golang")
q.Add("sort", "stars")
q.Add("order", "desc")

// 将查询参数设置回请求URL
req.URL.RawQuery = q.Encode()

fmt.Println("完整URL:", req.URL.String())
// 输出: 完整URL: https://api.example.com/search?order=desc&q=golang&sort=stars

设置请求超时

设置请求超时时间:

package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"net/http"
	"time"
)

func main() {
	// 创建带有超时的上下文
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	// 创建请求并设置上下文
	req, err := http.NewRequestWithContext(ctx, "GET", "https://httpbin.org/delay/10", nil)
	if err != nil {
		log.Fatalf("创建请求失败: %v", err)
	}

	// 发送请求
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatalf("请求失败: %v", err)
		// 可能输出: 请求失败: context deadline exceeded
	}
	defer resp.Body.Close()

	// 读取响应体
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatalf("读取响应失败: %v", err)
	}

	fmt.Printf("状态码: %d\n", resp.StatusCode)
	fmt.Printf("响应体: %s\n", body)
}

发送请求

使用 http.ClientDo 方法发送请求:

// 创建自定义客户端
client := &http.Client{
	Timeout: 10 * time.Second,
}

// 发送请求
resp, err := client.Do(req)
if err != nil {
	log.Fatalf("请求失败: %v", err)
}
defer resp.Body.Close()

处理 HTTP 响应

读取响应体

读取响应体内容:

// 读取响应体
body, err := io.ReadAll(resp.Body)
if err != nil {
	log.Fatalf("读取响应失败: %v", err)
}

// 转换为字符串
bodyStr := string(body)
fmt.Println("响应内容:", bodyStr)

处理状态码

检查和处理 HTTP 状态码:

// 检查状态码
switch resp.StatusCode {
case http.StatusOK, http.StatusCreated, http.StatusAccepted:
	fmt.Println("请求成功,状态码:", resp.StatusCode)
case http.StatusUnauthorized:
	fmt.Println("认证失败,需要登录")
case http.StatusForbidden:
	fmt.Println("没有权限访问该资源")
case http.StatusNotFound:
	fmt.Println("请求的资源不存在")
default:
	fmt.Printf("请求返回非预期状态码: %d\n", resp.StatusCode)
}

处理响应头

获取和处理响应头信息:

// 获取特定响应头
contentType := resp.Header.Get("Content-Type")
fmt.Println("Content-Type:", contentType)

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

解析 JSON 响应

解析 JSON 格式的响应:

package main

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

// 定义响应结构体
type User struct {
	Login     string `json:"login"`
	ID        int    `json:"id"`
	Name      string `json:"name"`
	Type      string `json:"type"`
	CreatedAt string `json:"created_at"`
}

func main() {
	// 发起 GET 请求
	resp, err := http.Get("https://api.github.com/users/golang")
	if err != nil {
		log.Fatalf("请求失败: %v", err)
	}
	defer resp.Body.Close()

	// 检查状态码
	if resp.StatusCode != http.StatusOK {
		log.Fatalf("请求返回非成功状态码: %d", resp.StatusCode)
	}

	// 读取响应体
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatalf("读取响应失败: %v", err)
	}

	// 解析 JSON 到结构体
	var user User
	if err := json.Unmarshal(body, &user); err != nil {
		log.Fatalf("解析 JSON 失败: %v", err)
	}

	// 使用解析后的数据
	fmt.Printf("用户信息:\n")
	fmt.Printf("  登录名: %s\n", user.Login)
	fmt.Printf("  ID: %d\n", user.ID)
	fmt.Printf("  名称: %s\n", user.Name)
	fmt.Printf("  类型: %s\n", user.Type)
	fmt.Printf("  创建时间: %s\n", user.CreatedAt)
}

解析 XML 响应

解析 XML 格式的响应:

package main

import (
	"encoding/xml"
	"fmt"
	"io"
	"log"
	"net/http"
)

// 定义 XML 响应结构体
type RSS struct {
	XMLName xml.Name `xml:"rss"`
	Channel Channel  `xml:"channel"`
}

type Channel struct {
	Title       string `xml:"title"`
	Description string `xml:"description"`
	Link        string `xml:"link"`
	Items       []Item `xml:"item"`
}

type Item struct {
	Title       string `xml:"title"`
	Description string `xml:"description"`
	Link        string `xml:"link"`
	PubDate     string `xml:"pubDate"`
}

func main() {
	// 发起 GET 请求获取 RSS 源
	resp, err := http.Get("https://blog.golang.org/feed.atom")
	if err != nil {
		log.Fatalf("请求失败: %v", err)
	}
	defer resp.Body.Close()

	// 检查状态码
	if resp.StatusCode != http.StatusOK {
		log.Fatalf("请求返回非成功状态码: %d", resp.StatusCode)
	}

	// 读取响应体
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatalf("读取响应失败: %v", err)
	}

	// 解析 XML
	var rss RSS
	if err := xml.Unmarshal(body, &rss); err != nil {
		log.Fatalf("解析 XML 失败: %v", err)
	}

	// 使用解析后的数据
	fmt.Printf("RSS 标题: %s\n", rss.Channel.Title)
	fmt.Printf("描述: %s\n", rss.Channel.Description)
	fmt.Printf("链接: %s\n", rss.Channel.Link)

	// 打印文章条目
	fmt.Println("\n文章列表:")
	for i, item := range rss.Channel.Items {
		if i >= 3 {
			break // 只显示前3条
		}
		fmt.Printf("\n%d. %s\n", i+1, item.Title)
		fmt.Printf("   发布日期: %s\n", item.PubDate)
		fmt.Printf("   链接: %s\n", item.Link)
	}
}

高级用法

HTTP 客户端定制

创建自定义 HTTP 客户端:

package main

import (
	"crypto/tls"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"time"
)

func main() {
	// 创建自定义传输层
	transport := &http.Transport{
		Proxy: http.ProxyFromEnvironment,
		DialContext: (&net.Dialer{
			Timeout:   30 * time.Second,
			KeepAlive: 30 * time.Second,
		}).DialContext,
		ForceAttemptHTTP2:     true,
		MaxIdleConns:          100,
		IdleConnTimeout:       90 * time.Second,
		TLSHandshakeTimeout:   10 * time.Second,
		ExpectContinueTimeout: 1 * time.Second,
		TLSClientConfig: &tls.Config{
			InsecureSkipVerify: false, // 生产环境不要设置为 true
		},
	}

	// 创建自定义客户端
	client := &http.Client{
		Transport: transport,
		Timeout:   15 * time.Second,
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			// 限制最多允许 10 次重定向
			if len(via) >= 10 {
				return fmt.Errorf("达到最大重定向次数")
			}
			return nil
		},
	}

	// 使用自定义客户端发起请求
	resp, err := client.Get("https://golang.org")
	if err != nil {
		log.Fatalf("请求失败: %v", err)
	}
	defer resp.Body.Close()

	// 读取响应体
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatalf("读取响应失败: %v", err)
	}

	fmt.Printf("状态码: %d\n", resp.StatusCode)
	fmt.Printf("响应体长度: %d 字节\n", len(body))
}

处理和设置 Cookie:

package main

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

func main() {
	// 创建 cookie jar
	jar, err := cookiejar.New(nil)
	if err != nil {
		log.Fatalf("创建 cookie jar 失败: %v", err)
	}

	// 创建带有 cookie jar 的客户端
	client := &http.Client{
		Jar: jar,
	}

	// 手动设置 cookie
	url1, _ := url.Parse("https://httpbin.org")
	cookies := []*http.Cookie{
		{
			Name:  "session_id",
			Value: "abc123",
		},
		{
			Name:  "user_id",
			Value: "42",
		},
	}
	jar.SetCookies(url1, cookies)

	// 发起请求,会自动带上 cookie
	resp, err := client.Get("https://httpbin.org/cookies")
	if err != nil {
		log.Fatalf("请求失败: %v", err)
	}
	defer resp.Body.Close()

	// 读取响应体
	body := make([]byte, 1024)
	n, _ := resp.Body.Read(body)

	fmt.Printf("状态码: %d\n", resp.StatusCode)
	fmt.Printf("响应体: %s\n", body[:n])

	// 获取服务器设置的 cookie
	fmt.Println("\n服务器设置的 Cookie:")
	for _, cookie := range resp.Cookies() {
		fmt.Printf("  %s = %s\n", cookie.Name, cookie.Value)
	}
}

上传文件

使用 multipart/form-data 上传文件:

package main

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

func main() {
	// 要上传的文件路径
	filePath := "example.txt"

	// 创建表单缓冲区
	var requestBody bytes.Buffer
	multipartWriter := multipart.NewWriter(&requestBody)

	// 创建文件表单字段
	fileWriter, err := multipartWriter.CreateFormFile("file", filepath.Base(filePath))
	if err != nil {
		log.Fatalf("创建表单文件字段失败: %v", err)
	}

	// 打开文件
	file, err := os.Open(filePath)
	if err != nil {
		log.Fatalf("打开文件失败: %v", err)
	}
	defer file.Close()

	// 将文件内容复制到表单字段
	_, err = io.Copy(fileWriter, file)
	if err != nil {
		log.Fatalf("复制文件内容失败: %v", err)
	}

	// 添加其他表单字段
	_ = multipartWriter.WriteField("description", "这是一个示例文件")
	_ = multipartWriter.WriteField("category", "documents")

	// 完成表单
	err = multipartWriter.Close()
	if err != nil {
		log.Fatalf("关闭表单失败: %v", err)
	}

	// 创建请求
	req, err := http.NewRequest("POST", "https://httpbin.org/post", &requestBody)
	if err != nil {
		log.Fatalf("创建请求失败: %v", err)
	}

	// 设置内容类型
	req.Header.Set("Content-Type", multipartWriter.FormDataContentType())

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

	// 读取响应
	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatalf("读取响应失败: %v", err)
	}

	fmt.Printf("状态码: %d\n", resp.StatusCode)
	fmt.Printf("响应体: %s\n", respBody)
}

下载文件

下载文件并显示进度:

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strconv"
	"time"
)

func main() {
	// 下载地址和保存路径
	url := "https://golang.org/dl/go1.17.linux-amd64.tar.gz"
	outputPath := "go1.17.linux-amd64.tar.gz"

	// 创建请求
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		log.Fatalf("创建请求失败: %v", err)
	}

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

	// 检查状态码
	if resp.StatusCode != http.StatusOK {
		log.Fatalf("请求返回非成功状态码: %d", resp.StatusCode)
	}

	// 获取文件大小
	contentLength := resp.Header.Get("Content-Length")
	size, err := strconv.ParseInt(contentLength, 10, 64)
	if err != nil {
		size = -1 // 未知大小
	}

	fmt.Printf("开始下载文件: %s\n", url)
	fmt.Printf("文件大小: %d 字节\n", size)

	// 创建输出文件
	output, err := os.Create(outputPath)
	if err != nil {
		log.Fatalf("创建文件失败: %v", err)
	}
	defer output.Close()

	// 创建进度报告器
	doneCh := make(chan struct{})
	go func() {
		ticker := time.NewTicker(500 * time.Millisecond)
		defer ticker.Stop()

		for {
			select {
			case <-ticker.C:
				info, err := output.Stat()
				if err != nil {
					continue
				}
				current := info.Size()

				if size > 0 {
					percent := float64(current) / float64(size) * 100
					fmt.Printf("\r下载进度: %.2f%% (%d/%d 字节)", percent, current, size)
				} else {
					fmt.Printf("\r已下载: %d 字节", current)
				}
			case <-doneCh:
				return
			}
		}
	}()

	// 复制数据到文件
	written, err := io.Copy(output, resp.Body)
	close(doneCh) // 停止进度报告

	if err != nil {
		log.Fatalf("\n下载失败: %v", err)
	}

	fmt.Printf("\n下载完成! 共 %d 字节已保存到 %s\n", written, outputPath)
}

HTTP/2 支持

Go 的 net/http 包默认支持 HTTP/2,无需额外配置:

package main

import (
	"fmt"
	"log"
	"net/http"
	"net/http/httputil"
)

func main() {
	// 创建请求
	req, err := http.NewRequest("GET", "https://http2.golang.org", nil)
	if err != nil {
		log.Fatalf("创建请求失败: %v", err)
	}

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

	// 检查协议版本
	fmt.Printf("协议版本: %s\n", resp.Proto)

	// 打印响应头
	dump, err := httputil.DumpResponse(resp, false)
	if err != nil {
		log.Fatalf("转储响应失败: %v", err)
	}

	fmt.Printf("响应头:\n%s\n", dump)
}

并发请求

使用 goroutine 和 channel 并发发送多个请求:

package main

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

// 请求结果结构体
type Result struct {
	URL      string
	Status   int
	Size     int
	Duration time.Duration
	Err      error
}

func main() {
	// 要请求的 URL 列表
	urls := []string{
		"https://golang.org",
		"https://github.com",
		"https://stackoverflow.com",
		"https://www.google.com",
		"https://www.cloudflare.com",
	}

	// 创建结果通道
	results := make(chan Result, len(urls))

	// 使用 WaitGroup 等待所有请求完成
	var wg sync.WaitGroup

	// 启动并发请求
	for _, url := range urls {
		wg.Add(1)
		go func(url string) {
			defer wg.Done()

			// 记录开始时间
			start := time.Now()

			// 发送请求
			resp, err := http.Get(url)
			result := Result{URL: url}

			if err != nil {
				result.Err = err
				results <- result
				return
			}

			// 读取响应体
			body, err := io.ReadAll(resp.Body)
			resp.Body.Close()

			// 计算请求时间
			duration := time.Since(start)

			// 填充结果
			result.Status = resp.StatusCode
			result.Duration = duration
			if err != nil {
				result.Err = err
			} else {
				result.Size = len(body)
			}

			// 发送结果
			results <- result
		}(url)
	}

	// 在后台等待所有请求完成并关闭结果通道
	go func() {
		wg.Wait()
		close(results)
	}()

	// 处理结果
	fmt.Println("并发请求结果:")
	fmt.Printf("%-30s %-10s %-10s %-15s\n", "URL", "状态", "大小", "耗时")
	fmt.Println(strings.Repeat("-", 70))

	for result := range results {
		if result.Err != nil {
			fmt.Printf("%-30s 错误: %v\n", result.URL, result.Err)
		} else {
			fmt.Printf("%-30s %-10d %-10d %-15s\n",
				result.URL,
				result.Status,
				result.Size,
				result.Duration,
			)
		}
	}
}

错误处理与调试

常见错误处理

处理常见的 HTTP 请求错误:

package main

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

func main() {
	// 创建请求
	req, err := http.NewRequest("GET", "https://nonexistent-domain-123456.com", nil)
	if err != nil {
		fmt.Printf("创建请求失败: %v\n", err)
		os.Exit(1)
	}

	// 设置超时上下文
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	req = req.WithContext(ctx)

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

	// 错误处理
	if err != nil {
		// 检查错误类型
		switch e := err.(type) {
		case *url.Error:
			// 超时错误
			if e.Timeout() {
				fmt.Println("请求超时")
			} else if ctx.Err() == context.Canceled {
				fmt.Println("请求被取消")
			} else {
				fmt.Printf("URL 错误: %v\n", e)
			}
		case *net.OpError:
			// 网络操作错误
			fmt.Printf("网络操作错误: %v\n", e)
			if e.Timeout() {
				fmt.Println("网络超时")
			}
			if e.Temporary() {
				fmt.Println("临时错误,可以重试")
			}
		case net.Error:
			// 网络错误
			fmt.Printf("网络错误: %v\n", e)
			if e.Timeout() {
				fmt.Println("网络超时")
			}
			if e.Temporary() {
				fmt.Println("临时错误,可以重试")
			}
		default:
			// 其他错误
			fmt.Printf("请求错误: %v\n", err)
		}
		os.Exit(1)
	}
	defer resp.Body.Close()

	// 处理响应...
	fmt.Printf("请求成功,状态码: %d\n", resp.StatusCode)
}

请求调试技巧

使用 httputil 包调试 HTTP 请求和响应:

package main

import (
	"fmt"
	"log"
	"net/http"
	"net/http/httputil"
	"strings"
)

func main() {
	// 创建请求
	data := strings.NewReader(`{"name":"gopher","message":"Hello, World!"}`)
	req, err := http.NewRequest("POST", "https://httpbin.org/post", data)
	if err != nil {
		log.Fatalf("创建请求失败: %v", err)
	}

	// 设置请求头
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", "Golang-HTTP-Client/1.0")

	// 转储请求内容(包括请求体)
	dumpReq, err := httputil.DumpRequestOut(req, true)
	if err != nil {
		log.Fatalf("转储请求失败: %v", err)
	}

	fmt.Println("===== 请求内容 =====")
	fmt.Printf("%s\n\n", dumpReq)

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

	// 转储响应内容(包括响应体)
	dumpResp, err := httputil.DumpResponse(resp, true)
	if err != nil {
		log.Fatalf("转储响应失败: %v", err)
	}

	fmt.Println("===== 响应内容 =====")
	fmt.Printf("%s\n", dumpResp)
}

最佳实践

连接池管理

合理配置 HTTP 客户端连接池:

package main

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

func main() {
	// 创建自定义传输层,配置连接池
	transport := &http.Transport{
		MaxIdleConns:        100,              // 最大空闲连接数
		MaxIdleConnsPerHost: 10,               // 每个主机的最大空闲连接数
		MaxConnsPerHost:     10,               // 每个主机的最大连接数
		IdleConnTimeout:     90 * time.Second, // 空闲连接超时时间
	}

	// 创建客户端
	client := &http.Client{
		Transport: transport,
		Timeout:   10 * time.Second,
	}

	// 使用客户端发送多个请求
	for i := 0; i < 5; i++ {
		fmt.Printf("发送请求 #%d\n", i+1)

		resp, err := client.Get("https://httpbin.org/get")
		if err != nil {
			log.Printf("请求 #%d 失败: %v\n", i+1, err)
			continue
		}

		// 读取并丢弃响应体
		_, err = io.Copy(io.Discard, resp.Body)
		resp.Body.Close() // 重要:必须关闭响应体以便连接复用

		if err != nil {
			log.Printf("读取响应 #%d 失败: %v\n", i+1, err)
		} else {
			fmt.Printf("请求 #%d 成功,状态码: %d\n", i+1, resp.StatusCode)
		}

		// 模拟处理时间
		time.Sleep(500 * time.Millisecond)
	}

	fmt.Println("所有请求完成")
}

请求重试策略

实现简单的请求重试策略:

package main

import (
	"fmt"
	"io"
	"log"
	"math"
	"net/http"
	"time"
)

// 带重试的 HTTP GET 请求
func getWithRetry(url string, maxRetries int) (*http.Response, error) {
	// 创建客户端
	client := &http.Client{
		Timeout: 10 * time.Second,
	}

	// 重试计数
	var resp *http.Response
	var err error

	for attempt := 0; attempt <= maxRetries; attempt++ {
		// 如果不是第一次尝试,则等待一段时间
		if attempt > 0 {
			// 指数退避策略:等待时间随重试次数指数增长
			waitTime := time.Duration(math.Pow(2, float64(attempt))) * time.Second
			fmt.Printf("重试 #%d,等待 %v...\n", attempt, waitTime)
			time.Sleep(waitTime)
		}

		// 发送请求
		fmt.Printf("尝试 #%d: 请求 %s\n", attempt, url)
		resp, err = client.Get(url)

		// 请求成功
		if err == nil {
			// 检查状态码,某些状态码可能需要重试
			if resp.StatusCode >= 200 && resp.StatusCode < 300 {
				return resp, nil // 成功
			} else if resp.StatusCode >= 500 {
				// 服务器错误,关闭响应体并重试
				fmt.Printf("服务器错误 (状态码: %d),准备重试\n", resp.StatusCode)
				resp.Body.Close()
				continue
			} else {
				// 其他状态码,不重试
				return resp, nil
			}
		}

		// 请求失败,检查是否是临时错误
		fmt.Printf("请求失败: %v\n", err)
	}

	// 达到最大重试次数
	return resp, fmt.Errorf("达到最大重试次数 (%d),最后一个错误: %v", maxRetries, err)
}

func main() {
	// 使用重试函数发送请求
	resp, err := getWithRetry("https://httpbin.org/status/500", 3)
	if err != nil {
		log.Fatalf("请求失败: %v", err)
	}
	defer resp.Body.Close()

	// 读取响应体
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatalf("读取响应失败: %v", err)
	}

	fmt.Printf("状态码: %d\n", resp.StatusCode)
	fmt.Printf("响应体: %s\n", body)
}

性能优化

HTTP 请求性能优化技巧:

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"net/http/httptrace"
	"time"
)

func main() {
	// 创建请求
	req, err := http.NewRequest("GET", "https://golang.org", nil)
	if err != nil {
		log.Fatalf("创建请求失败: %v", err)
	}

	// 创建跟踪上下文
	trace := &httptrace.ClientTrace{
		GetConn: func(hostPort string) {
			fmt.Printf("获取连接: %s\n", hostPort)
		},
		GotConn: func(info httptrace.GotConnInfo) {
			fmt.Printf("获得连接: 复用=%v, 空闲时间=%v\n", info.Reused, info.IdleTime)
		},
		ConnectStart: func(network, addr string) {
			fmt.Printf("开始连接: %s %s\n", network, addr)
		},
		ConnectDone: func(network, addr string, err error) {
			fmt.Printf("连接完成: %s %s, 错误=%v\n", network, addr, err)
		},
		DNSStart: func(info httptrace.DNSStartInfo) {
			fmt.Printf("开始DNS查询: %s\n", info.Host)
		},
		DNSDone: func(info httptrace.DNSDoneInfo) {
			fmt.Printf("DNS查询完成: %v, 错误=%v\n", info.Addrs, info.Err)
		},
		TLSHandshakeStart: func() {
			fmt.Println("开始TLS握手")
		},
		TLSHandshakeDone: func(state httptrace.TLSConnectionState, err error) {
			fmt.Printf("TLS握手完成: 协议=%s, 错误=%v\n", state.NegotiatedProtocol, err)
		},
		WroteRequest: func(info httptrace.WroteRequestInfo) {
			fmt.Printf("请求已发送: 错误=%v\n", info.Err)
		},
		Got100Continue: func() {
			fmt.Println("收到 100 Continue 响应")
		},
		Got1xxResponse: func(code int, header http.Header) {
			fmt.Printf("收到 1xx 响应: 状态码=%d\n", code)
		},
	}

	// 将跟踪上下文与请求关联
	req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))

	// 记录开始时间
	start := time.Now()

	// 发送请求
	client := &http.Client{
		// 禁用压缩以便观察实际传输大小
		Transport: &http.Transport{
			DisableCompression: true,
		},
	}

	resp, err := client.Do(req)
	if err != nil {
		log.Fatalf("请求失败: %v", err)
	}
	defer resp.Body.Close()

	// 读取响应体并丢弃
	n, err := io.Copy(io.Discard, resp.Body)
	if err != nil {
		log.Fatalf("读取响应失败: %v", err)
	}

	// 计算总时间
	duration := time.Since(start)

	// 打印性能统计
	fmt.Printf("\n性能统计:\n")
	fmt.Printf("总时间: %v\n", duration)
	fmt.Printf("状态码: %d\n", resp.StatusCode)
	fmt.Printf("响应大小: %d 字节\n", n)
	fmt.Printf("吞吐量: %.2f KB/s\n", float64(n)/1024/duration.Seconds())
}

完整示例

一个综合的 HTTP 客户端示例,包含错误处理、重试、超时和日志记录:

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"math"
	"net/http"
	"net/http/httputil"
	"os"
	"time"
)

// 自定义 HTTP 客户端
type HTTPClient struct {
	client     *http.Client
	baseURL    string
	debug      bool
	maxRetries int
}

// 创建新的 HTTP 客户端
func NewHTTPClient(baseURL string, timeout time.Duration, debug bool, maxRetries int) *HTTPClient {
	transport := &http.Transport{
		MaxIdleConns:        100,
		MaxIdleConnsPerHost: 10,
		IdleConnTimeout:     90 * time.Second,
	}

	client := &http.Client{
		Transport: transport,
		Timeout:   timeout,
	}

	return &HTTPClient{
		client:     client,
		baseURL:    baseURL,
		debug:      debug,
		maxRetries: maxRetries,
	}
}

// 发送 GET 请求
func (c *HTTPClient) Get(ctx context.Context, path string, headers map[string]string) ([]byte, int, error) {
	url := c.baseURL + path
	return c.sendRequest(ctx, "GET", url, nil, headers)
}

// 发送 POST 请求
func (c *HTTPClient) Post(ctx context.Context, path string, body io.Reader, headers map[string]string) ([]byte, int, error) {
	url := c.baseURL + path
	return c.sendRequest(ctx, "POST", url, body, headers)
}

// 发送请求并处理重试
func (c *HTTPClient) sendRequest(ctx context.Context, method, url string, body io.Reader, headers map[string]string) ([]byte, int, error) {
	var resp *http.Response
	var respBody []byte
	var err error

	// 重试循环
	for attempt := 0; attempt <= c.maxRetries; attempt++ {
		// 如果不是第一次尝试,则等待一段时间
		if attempt > 0 {
			// 指数退避策略
			waitTime := time.Duration(math.Pow(2, float64(attempt))) * time.Second
			log.Printf("重试 #%d,等待 %v...", attempt, waitTime)
			
			// 使用带有超时的上下文
			select {
			case <-time.After(waitTime):
				// 继续执行
			case <-ctx.Done():
				return nil, 0, ctx.Err()
			}
		}

		// 创建新的请求
		req, err := http.NewRequestWithContext(ctx, method, url, body)
		if err != nil {
			return nil, 0, fmt.Errorf("创建请求失败: %w", err)
		}

		// 设置请求头
		for k, v := range headers {
			req.Header.Set(k, v)
		}

		// 调试模式:打印请求内容
		if c.debug {
			dumpReq, err := httputil.DumpRequestOut(req, true)
			if err == nil {
				log.Printf("请求:\n%s\n", dumpReq)
			}
		}

		// 发送请求
		resp, err = c.client.Do(req)

		// 处理错误
		if err != nil {
			log.Printf("请求失败 (尝试 #%d): %v", attempt, err)
			
			// 检查是否是超时或临时错误,如果是则重试
			if attempt < c.maxRetries {
				continue
			}
			return nil, 0, err
		}

		// 调试模式:打印响应内容
		if c.debug {
			dumpResp, err := httputil.DumpResponse(resp, true)
			if err == nil {
				log.Printf("响应:\n%s\n", dumpResp)
			}
		}

		// 读取响应体
		respBody, err = io.ReadAll(resp.Body)
		resp.Body.Close()

		if err != nil {
			log.Printf("读取响应失败 (尝试 #%d): %v", attempt, err)
			if attempt < c.maxRetries {
				continue
			}
			return nil, resp.StatusCode, err
		}

		// 检查状态码
		if resp.StatusCode >= 500 && attempt < c.maxRetries {
			// 服务器错误,重试
			log.Printf("服务器错误 (状态码: %d),准备重试", resp.StatusCode)
			continue
		}

		// 成功或不需要重试的状态码
		break
	}

	// 返回结果
	statusCode := 0
	if resp != nil {
		statusCode = resp.StatusCode
	}

	return respBody, statusCode, err
}

// 发送 JSON 请求并解析 JSON 响应
func (c *HTTPClient) SendJSON(ctx context.Context, method, path string, requestBody interface{}, responseBody interface{}) (int, error) {
	// 将请求体编码为 JSON
	var bodyReader io.Reader
	if requestBody != nil {
		jsonData, err := json.Marshal(requestBody)
		if err != nil {
			return 0, fmt.Errorf("编码请求体失败: %w", err)
		}
		bodyReader = bytes.NewReader(jsonData)
	}

	// 设置请求头
	headers := map[string]string{
		"Content-Type": "application/json",
		"Accept":       "application/json",
	}

	// 发送请求
	respData, statusCode, err := c.sendRequest(ctx, method, path, bodyReader, headers)
	if err != nil {
		return statusCode, err
	}

	// 解析响应体
	if responseBody != nil && len(respData) > 0 {
		if err := json.Unmarshal(respData, responseBody); err != nil {
			return statusCode, fmt.Errorf("解析响应体失败: %w\n响应内容: %s", err, respData)
		}
	}

	return statusCode, nil
}

// 示例响应结构体
type Repository struct {
	ID          int    `json:"id"`
	Name        string `json:"name"`
	FullName    string `json:"full_name"`
	Description string `json:"description"`
	StarCount   int    `json:"stargazers_count"`
	ForkCount   int    `json:"forks_count"`
	URL         string `json:"html_url"`
}

func main() {
	// 创建自定义 HTTP 客户端
	client := NewHTTPClient(
		"https://api.github.com",
		10*time.Second,
		true, // 启用调试
		2,   // 最大重试次数
	)

	// 创建上下文
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	// 获取 Go 仓库信息
	var repo Repository
	statusCode, err := client.SendJSON(ctx, "GET", "/repos/golang/go", nil, &repo)

	if err != nil {
		log.Fatalf("请求失败: %v", err)
	}

	// 打印结果
	fmt.Printf("状态码: %d\n", statusCode)
	fmt.Printf("仓库信息:\n")
	fmt.Printf("  ID: %d\n", repo.ID)
	fmt.Printf("  名称: %s\n", repo.Name)
	fmt.Printf("  完整名称: %s\n", repo.FullName)
	fmt.Printf("  描述: %s\n", repo.Description)
	fmt.Printf("  星标数: %d\n", repo.StarCount)
	fmt.Printf("  分支数: %d\n", repo.ForkCount)
	fmt.Printf("  URL: %s\n", repo.URL)
}

通过本文的学习,你应该已经掌握了在 Go 中发起各种 HTTP 请求、处理响应以及一些高级用法和最佳实践。Go 的 net/http 包提供了强大而灵活的 HTTP 客户端和服务器实现,可以满足从简单到复杂的各种 HTTP 通信需求。