Go语言的net/http
包不仅提供了HTTP服务器功能,还包含了功能强大的HTTP客户端API,让我们能够轻松地与各种Web服务进行交互。
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))
}
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响应的核心是正确读取响应体。前面我们已经看到了使用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
包提供的默认客户端来实现。
client := &http.Client{}
// 使用自定义客户端发送请求
resp, err := client.Get("https://example.com")
// ...
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 := &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
可以实现请求取消和超时控制:
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()
// 处理响应
// ...
}
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
格式:
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()
// 处理响应
// ...
}
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)
// ...
}
对于重复调用同一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
内部使用连接池机制来提高性能:
一些关键的性能优化点:
http.Client
// 创建一个全局的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
}
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"
}