引言
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.Client 的 Do 方法发送请求:
// 创建自定义客户端
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
处理和设置 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 通信需求。