玩转 Go HTTP 客户端系列(一)—— 原生 net/http 库基础用法详解

Go Native net/http 客户端基础用法详解

写在前面

Go 原生的 net/http 库提供了强大的 HTTP 客户端功能。本篇将以代码的方式讲解其基础用法,包括GETPOSTPUTDELETE 请求,以及一些常见的请求响应对象的数据处理方法,还包括文件上传和下载等操作。

对于初学者来说,建议在使用封装库之前,先熟悉原生库的基本用法。掌握原生库的基本操作是打好基础的关键,无论后续你使用多么高级的封装库,都能理解其底层原理和工作方式。原汁原味的学习和实践将有助于你更好地掌握 Go 语言。

以下内容将持续更新...

开发和调试站点

httpbin.org 是一个用于 HTTP 请求和响应测试的网站。它允许开发者发送各种类型的 HTTP 请求,并获取与之相关的信息和数据。这个网站可以用来测试你的 HTTP 客户端代码,确保其在不同情况下正常工作。

httpbin 提供了一系列有用的 endpoint,包括:

  1. /get:返回关于 GET 请求的信息。
  2. /post:接受 POST 请求并返回请求数据。
  3. /put:接受 PUT 请求并返回请求数据。
  4. /delete:接受 DELETE 请求并返回请求数据。
  5. /status/:code:返回指定状态码的响应。
  6. /redirect/:n:执行指定次数的重定向。
  7. /headers:返回 HTTP 头部信息。
  8. /ip:返回发起请求的 IP 地址。
  9. /user-agent:返回 User-Agent 头部信息。

通过访问这些端点,你可以模拟不同类型的 HTTP 请求和获取相应的响应,用于测试和调试你的 HTTP 客户端代码。这对于开发和调试 HTTP 相关的功能非常有用。

请求方法 Request Method

GET 请求

函数签名:func Get(url string) (resp *Response, err error)

go 复制代码
// 发送 GET 请求(语法糖)
resp, err := http.Get("https://httpbin.org/get")
if err != nil {
    panic(err)
}

POST 请求

函数签名:func Post(url, contentType string, body io.Reader) (resp *Response, err error)

go 复制代码
// 发送 POST 请求(语法糖)
resp, err := http.Post("https://httpbin.org/post", "application/json", nil)
if err != nil {
    panic(err)
}

PUT 请求

除了 Get,Post 其它不常用的方法 😭 就不提供语法糖了。

go 复制代码
// 创建 PUT 请求
req, err := http.NewRequest(http.MethodPut, "https://httpbin.org/put", nil)
if err != nil {
    panic(err)
}

// 设置请求头
req.Header.Set("Content-Type", "application/json")

// 发送请求
resp, err := http.DefaultClient.Do(req)
if err != nil {
    panic(err)
}

DELETE 请求

go 复制代码
// 创建 DELETE 请求
req, err := http.NewRequest(http.MethodDelete, "https://httpbin.org/delete", nil)
if err != nil {
    panic(err)
}

// 发送请求
resp, err := http.DefaultClient.Do(req)
if err != nil {
    panic(err)
}

请求参数 Query Parameters

URL 硬编码

如果你愿意用这种方式编写 URL 请求参数,那么没有人会阻止你:

go 复制代码
// 发送 GET 请求 + 携带请求参数
resp, err := http.Get("https://httpbin.org/get?page_num=1&page_size=10")
if err != nil {
    panic(err)
}

使用 url.Values 类型及方法

虽然这种方式可能会增加一些代码量,但它可以帮助我们更灵活地构建和修改请求参数。

go 复制代码
// 创建 GET 请求
req, err := http.NewRequest(http.MethodGet, "https://httpbin.org/get", nil)
if err != nil {
    panic(err)
}

// 设置请求参数
params := make(url.Values)
params.Set("page_num", "1")   // set 会覆盖
params.Add("page_size", "10") // add 仅追加
req.URL.RawQuery = params.Encode()

// 发送请求
resp, err := http.DefaultClient.Do(req)
if err != nil {
    panic(err)
}

请求头 Request Headers

go 复制代码
// 创建 GET 请求
req, err := http.NewRequest(http.MethodGet, "https://httpbin.org/get", nil)
if err != nil {
    panic(err)
}

// 设置请求头
req.Header.Add("Accept", "*/*")
req.Header.Add("Accept-Language", "en-US,en;q=0.9")
req.Header.Add("Authorization", "Token 12345")
req.Header.Add("User-Agent", "Go-net/http")

// 发送请求
resp, err := http.DefaultClient.Do(req)
if err != nil {
    panic(err)
}

请求体 Request Body

TEXT 类型

MIME Type:"text/plain"

go 复制代码
// 创建一个包含纯文本数据的字节缓冲
textData := []byte("This is a plain text request body.")
bodyBuf := bytes.NewBuffer(textData)

// 发送 POST 请求
resp, err := http.Post("https://httpbin.org/post", "text/plain", bodyBuf)
if err != nil {
    panic(err)
}

JSON 类型

MIME Type:"application/json"

go 复制代码
// 定义请求体
type Userinfo struct {
    Name   string `json:"name"`  
    Age    int    `json:"age"`  
    Gender bool   `json:"gender"`
}

// 创建一个 Userinfo 结构体实例
user := Userinfo{
    Name:   "John Doe",
    Age:    30,
    Gender: true,
}

// 将 Userinfo 结构体转换为 JSON 字符串
jsonData, _ := json.Marshal(user)

// 创建一个包含 JSON 数据的字节缓冲
bodyBuf := bytes.NewBuffer(jsonData)

// 发送 POST 请求
resp, err := http.Post("https://httpbin.org/post", "application/json", bodyBuf)
if err != nil {
    panic(err)
}

请求表单 Request Form

使用 url.Values 类型及方法

像上面请求参数那样,同样使用 url.Values 的方式来构建请求表单。

go 复制代码
// 创建一个包含表单数据的 url.Values
form := url.Values{}
form.Add("account", "your_account_value")
form.Add("password", "your_password_value")

// 将表单数据转换为字符串形式
formStr := form.Encode()

// 创建一个包含表单数据的请求体
bodyBuf := strings.NewReader(formStr)

// 发送 POST 请求
resp, err := http.Post("https://httpbin.org/post", "application/x-www-form-urlencoded", bodyBuf)
if err != nil {
    panic(err)
}

使用 map 自行构建表单形式

如果您愿意,也可以使用 map[string]string 来表示表单数据,并手动将其编码为表单数据格式,然后将其放入请求体中。

go 复制代码
// 创建一个包含表单数据的 map
formData := map[string]string{
    "account":  "your_account_value",
    "password": "your_password_value",
}

// 将表单数据编码为表单数据格式
var formStr string
for key, value := range formData {
    formStr += key + "=" + url.QueryEscape(value) + "&"
}

// 去除末尾的 "&"
if len(formStr) > 0 {
    formStr = formStr[:len(formStr)-1]
}

// 创建一个包含表单数据的请求体
bodyBuf := strings.NewReader(formStr)

// 发送 POST 请求
resp, err := http.Post("https://httpbin.org/post", "application/x-www-form-urlencoded", bodyBuf)
if err != nil {
    panic(err)
}

文件上传

go 复制代码
package main  
  
import (
    "bytes"  
    "fmt"  
    "io"  
    "mime/multipart"  
    "net/http"  
    "os"  
)

func main() {  
    // 打开本地的 JPG 文件
    file, err := os.Open("demo.jpg")
    if err != nil {
        panic(err)
    }
    defer func() { _ = file.Close() }()

    // 创建一个字节缓冲用于构建请求体
    bodyBuf := &bytes.Buffer{}

    // 创建一个新的 multipart writer
    writer := multipart.NewWriter(bodyBuf)

    // 创建一个包含 JPG 文件的文件字段
    fileField, err := writer.CreateFormFile("jpg_file", "demo.jpg")
    if err != nil {
        panic(err)
    }

    // 将 JPG 文件内容复制到文件字段中
    _, err = io.Copy(fileField, file)
    if err != nil {
        panic(err)
    }

    // 必须关闭 multipart writer,以便添加结束符
    defer func() { _ = writer.Close() }()

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

    // 设置请求头的 Content-Type 为 multipart/form-data
    req.Header.Set("Content-Type", writer.FormDataContentType())

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

    // 处理响应
    fmt.Println("Response Status:", resp.Status)
}

两种表单 MIME 格式区别

multipart/form-dataapplication/x-www-form-urlencoded 是两种常见的表单 MIME 格式,它们之间的主要区别在于数据编码和文件上传支持:

application/x-www-form-urlencoded

  • 数据编码方式 :使用 URL 编码(URL encoding)将表单数据编码为一个字符串,k-v 键值对之间用 & 分隔,特殊字符会被转义(例如,空格转换为 %20)。

  • 文件上传:不支持文件上传,仅适用于普通文本表单字段。

  • 数据体积:通常用于小型表单,因为它会对数据进行编码,可能会导致较大的数据体积。

这种格式适合处理普通简单的表单数据,如用户名、密码等,但不适合上传文件或二进制数据。

multipart/form-data

  • 数据编码方式 :使用多部分编码(multipart encoding),将表单数据分割成多个部分,每个部分包含一个字段的数据,以及可选的头信息(例如,文件名、类型等)。每个部分之间用一个 boundary 边界标识符分隔。

  • 文件上传:支持文件上传,可以包含二进制文件数据。

  • 数据体积:更适合传输大型二进制文件,因为它不会对数据进行编码。

这种格式适用于包含文件上传功能的表单,可以用于上传图片、音视频文件等二进制数据。

Get和Post比较

Get 请求

  • 使用 URL 编码方式提交查询参数,通常限制在几千个字符以内(具体限制取决于浏览器和服务器的配置)。
  • 通常不接受请求体(虽然也可以携带,但不符合标准用法)。

Post 请求

通常有三种形式的 Payload 载体

  • Json 格式:可以用于传递大量数据,例如 JSON 格式的请求体。
  • Form 表单:与 GET 请求的查询参数格式相同,但是这些数据被包含在请求体中传递。
  • Media 文件:可以通过 POST 请求上传媒体文件等二进制数据。

请求和响应对象

Response 信息

可以从响应对象中获取响应体、响应信息、响应状态码、响应头等内容

go 复制代码
resp, err := http.Get("https://httpbin.org/get")
if err != nil {
    panic(err)
}

// 读取响应体
responseData, err := io.ReadAll(resp.Body)
if err != nil {
    panic(err)
}
defer func() { _ = resp.Body.Close() }()
fmt.Printf("%s", responseData)

// 输出响应的状态码
fmt.Println(resp.StatusCode)                 // 200

// 输出响应的状态描述
fmt.Println(resp.Status)                     // 200 OK

// 输出响应的 HTTP 协议版本
fmt.Println(resp.Proto)                      // HTTP/2.0

// 输出响应的内容长度(字节数)
fmt.Println(resp.ContentLength)              // 272

// 输出响应头的字段
fmt.Println(resp.Header.Get("Content-Type")) // application/json

Request 信息

也可以从响应对象中拿到请求的相关信息

go 复制代码
resp, err := http.Get("https://httpbin.org/get")
if err != nil {
    panic(err)
}

// 获取请求host
fmt.Println(resp.Request.Host)               // httpbin.org

// 获取请求method
fmt.Println(resp.Request.Method)             // GET

// 获取请求url
fmt.Println(resp.Request.URL)                // https://httpbin.org/get

// 获取请求header
fmt.Println(resp.Request.Header)             // map[]

// 获取请求form
fmt.Println(resp.Request.Form)               // map[]

字符编码 Character Encoding

常见的 MIME 类型

列表参考:developer.mozilla.org/en-US/docs/...

获取网页编码的方式

html.spec.whatwg.org/multipage/p...

以下方法可以用于确定网页或文档的字符编码,以便正确地解析其中的文本内容。

  1. Content-Type 响应头字段 :当服务器响应 HTTP 请求时,通常会在响应头中包含 Content-Type 字段,用于指示响应主体的媒体类型和字符集(可选)。例如,如果服务器返回的是 HTML 页面,Content-Type 可能会设置为 Content-Type: text/html; charset=utf-8,其中 charset=utf-8 表示字符集为 UTF-8

  2. HTML 文本中的 <meta> 标签 :在 HTML 文档的 <head> 部分,通常会包含 <meta> 标签,用于指定文档的字符集。例如,<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 表示文档使用 UTF-8 字符集。

  3. 通过分析网页的响应头部来猜测编码 :在某些情况下,服务器可能没有明确指定 Content-Type 的字符集部分,或者 HTML 中没有 <meta> 标签来指定字符集。在这种情况下,可以尝试根据响应头部的其他信息来猜测网页的编码方式,尽管这种方式不够准确。

解析 Content-Type 头

go 复制代码
// 创建 GET 请求
resp, err := http.Get("https://www.google.com")
if err != nil {
    panic(err)
}

// 获取响应的 Content-Type 头部字段
contentType := resp.Header.Get("Content-Type")

// 使用 switch 语句判断响应内容类型
switch {
case strings.Contains(contentType, "text/html"):
    fmt.Println("HTML 页面")
case strings.Contains(contentType, "text/plain"):
    fmt.Println("纯文本")
case strings.Contains(contentType, "application/json"):
    fmt.Println("JSON 数据")
case strings.Contains(contentType, "application/xml"), strings.Contains(contentType, "text/xml"):
    fmt.Println("XML 数据")
case strings.Contains(contentType, "application/octet-stream"):
    fmt.Println("二进制数据")
case strings.Contains(contentType, "application/pdf"):
    fmt.Println("PDF 文档")
case strings.Contains(contentType, "image/jpeg"), strings.Contains(contentType, "image/png"):
    fmt.Println("图片")
case strings.Contains(contentType, "audio/mpeg"), strings.Contains(contentType, "audio/wav"):
    fmt.Println("音频")
case strings.Contains(contentType, "video/mp4"), strings.Contains(contentType, "video/avi"):
    fmt.Println("视频")
default:
    fmt.Println("其他类型的数据")
}

自动解析网页的字符编码

我们以中国台湾网的 GBK 编码(非 UTF-8)来举例。

打开 Inspector 查看 HTTP 请求的报文。

如果不是默认的 UTF-8 编码,则 charset.DetermineEncoding 函数无法解析,需要通过 transform.NewReader 手动设置,否则直接解析会乱码掉。

以下代码可自动解析网页中任意字符编码类型

go 复制代码
package main

import (
    "bufio"
    "fmt"
    "net/http"

    "golang.org/x/net/html/charset"
    "golang.org/x/text/transform"
)

func main() {
    // resp, err := http.Get("http://www.baidu.com/") // utf8编码
    resp, err := http.Get("http://www.taiwan.cn/")    // gbk,非utf8编码
    if err != nil {
        panic(err)
    }
    defer func() { _ = resp.Body.Close() }()

    // 创建一个带缓冲的读取器,用于从响应主体中读取数据
    bufReader := bufio.NewReader(resp.Body)

    // 从响应主体中预读取最多 1024 字节的数据
    bytes, _ := bufReader.Peek(1024) // 预扫描字节流以确定其编码,限制为前1024个字节

    // 获取响应头中的 Content-Type 字段,该字段指示了响应主体的媒体类型和字符集信息
    contentType := resp.Header.Get("Content-Type")

    // 尝试自动检测网页的编码方式,会根据预读取的数据和 Content-Type 字段的值来推断字符集
    e, name, certain := charset.DetermineEncoding(bytes, contentType)

    // 打印检测到的字符集编码方式
    fmt.Println(e)

    // 打印字符集的标准名称
    fmt.Println(name)

    // 打印是否检测到了字符集,如果 certain 为 true,表示检测到了字符集,否则可能是默认值
    fmt.Println(certain)

    // 是否能够确认字符编码
    var content []byte
    if certain && name == "utf-8" {
        // 能够正常解码utf-8的网页内容
        content, _ = io.ReadAll(resp.Body)
    } else {
        // 使用新的decder来解析网页内容
        bodyReader := transform.NewReader(bufReader, e.NewDecoder())
        content, _ = io.ReadAll(bodyReader)
    }

    // 打印网页html源码
    fmt.Println(string(content))
}
相关推荐
捂月40 分钟前
Spring Boot 深度解析:快速构建高效、现代化的 Web 应用程序
前端·spring boot·后端
煎鱼eddycjy1 小时前
Go 语言十五周年!权力交接、回顾与展望
go
瓜牛_gn1 小时前
依赖注入注解
java·后端·spring
Estar.Lee1 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
喜欢猪猪1 小时前
Django:从入门到精通
后端·python·django
一个小坑货1 小时前
Cargo Rust 的包管理器
开发语言·后端·rust
bluebonnet271 小时前
【Rust练习】22.HashMap
开发语言·后端·rust
uhakadotcom2 小时前
如何实现一个基于CLI终端的AI 聊天机器人?
后端
Iced_Sheep2 小时前
干掉 if else 之策略模式
后端·设计模式
XINGTECODE3 小时前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang