引言

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

目录

基础 HTTP 请求

GET 请求

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 请求用于向服务器提交数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 创建自定义请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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 字段设置请求头:

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

设置查询参数

设置 URL 查询参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建请求
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

设置请求超时

设置请求超时时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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 方法发送请求:

1
2
3
4
5
6
7
8
9
10
11
// 创建自定义客户端
client := &http.Client{
Timeout: 10 * time.Second,
}

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

处理 HTTP 响应

读取响应体

读取响应体内容:

1
2
3
4
5
6
7
8
9
// 读取响应体
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalf("读取响应失败: %v", err)
}

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

处理状态码

检查和处理 HTTP 状态码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 检查状态码
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)
}

处理响应头

获取和处理响应头信息:

1
2
3
4
5
6
7
8
9
10
11
// 获取特定响应头
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 格式的响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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 格式的响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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 客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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 上传文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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)
}

下载文件

下载文件并显示进度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
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,无需额外配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 并发发送多个请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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 请求错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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 请求和响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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 客户端连接池:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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("所有请求完成")
}

请求重试策略

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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 请求性能优化技巧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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 客户端示例,包含错误处理、重试、超时和日志记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
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 通信需求。