Go Native net/http 客户端基础用法详解
写在前面
Go
原生的 net/http
库提供了强大的 HTTP
客户端功能。本篇将以代码的方式讲解其基础用法,包括GET
、POST
、PUT
、DELETE
请求,以及一些常见的请求响应对象的数据处理方法,还包括文件上传和下载等操作。
对于初学者来说,建议在使用封装库之前,先熟悉原生库的基本用法。掌握原生库的基本操作是打好基础的关键,无论后续你使用多么高级的封装库,都能理解其底层原理和工作方式。原汁原味的学习和实践将有助于你更好地掌握 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
相关的功能非常有用。
请求方法 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-data
和 application/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
)来举例。
打开 Inspector
查看 HTTP
请求的报文。
如果不是默认的 UTF-8
编码,则 charset.DetermineEncoding
函数无法解析,需要通过 transform.NewReader
手动设置,否则直接解析会乱码掉。
以下代码可自动解析网页中任意字符编码类型
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))
}