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

2023年 10月 3日 62.5k 0

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

写在前面

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

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

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

开发和调试站点

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

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

  • /get:返回关于 GET 请求的信息。
  • /post:接受 POST 请求并返回请求数据。
  • /put:接受 PUT 请求并返回请求数据。
  • /delete:接受 DELETE 请求并返回请求数据。
  • /status/:code:返回指定状态码的响应。
  • /redirect/:n:执行指定次数的重定向。
  • /headers:返回 HTTP 头部信息。
  • /ip:返回发起请求的 IP 地址。
  • /user-agent:返回 User-Agent 头部信息。
  • 通过访问这些端点,你可以模拟不同类型的 HTTP 请求和获取相应的响应,用于测试和调试你的 HTTP 客户端代码。这对于开发和调试 HTTP 相关的功能非常有用。

    image.png

    请求方法 Request Method

    GET 请求

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

    // 发送 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)

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

    PUT 请求

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

    // 创建 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 请求

    // 创建 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 请求参数,那么没有人会阻止你:

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

    使用 url.Values 类型及方法

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

    // 创建 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

    // 创建 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"

    // 创建一个包含纯文本数据的字节缓冲
    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"

    // 定义请求体
    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 的方式来构建请求表单。

    // 创建一个包含表单数据的 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 来表示表单数据,并手动将其编码为表单数据格式,然后将其放入请求体中。

    // 创建一个包含表单数据的 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)
    }
    

    文件上传

    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 信息

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

    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 信息

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

    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…

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

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

  • HTML 文本中的 标签:在 HTML 文档的 部分,通常会包含 标签,用于指定文档的字符集。例如, 表示文档使用 UTF-8 字符集。

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

  • 解析 Content-Type 头

    // 创建 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)来举例。

    image.png

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

    image.png

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

    image.png

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

    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))
    }
    

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论