Golang net/http客户端的使用

Posted 玩家_名狱

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang net/http客户端的使用相关的知识,希望对你有一定的参考价值。

测试连接:有个站点,专门为了测试http、https请求,get请求为http://httpbin.org/get、post请求为http://httpbin.org/post、put请求为http://httpbin.org/put等等。发送一个请求给它,他就根据我们请求内容返回我们的请求内容给我们,这样我们就可知道我们发送的结果是什么了。

下面模拟各种方法的请求,各个方法的创建基本都差不多,但是由于Get和Post方法经常使用,所以官方帮我们做了包装,我们只需要一个函数调用就可以了,但是没有包装其他的方法,所以我们要了解基本的创建,再了解包装后的get和post。其实在使用过程中,我们常用的还是基本的创建方法,因为使用过程中我们需要定制一些参数,而包装后的做不了修改。

基本的创建

以get方法为例:

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
)
func main() 
    // 创建一个请求对象,返回请求对象。传入参数为:请求方法,url,body要传递的数据。
	req, err := http.NewRequest(http.MethodGet, "http://httpbin.org/get", nil)
	if err != nil 
		panic(err)
	
    // 虽然我们创建了请求对象,但是没有执行真正的请求。DefaultClient是Client对象,该对象包含了所有的客户端操作;Do方法执行请求,然后返回响应对象
	resp, err := http.DefaultClient.Do(req)
	if err != nil 
		panic(err)
	
    // 退出时关闭响应连接
	defer resp.Body.Close()
    // 获取响应数据,iout.ReadAll方法获取Reader对象的数据,resp.Body就是Reader对象
	respData, err := ioutil.ReadAll(resp.Body)
	if err != nil 
		panic(err)
	
    // 打印响应的数据
	fmt.Printf("%s", respData)

创建请求对象时,传入参数的请求方法可以写"GET"也可以写上面那个,上面那个是net/http包里面帮我们写好的,其他方法都写好了,包中的源码为:

const (
	MethodGet     = "GET"
	MethodHead    = "HEAD"
	MethodPost    = "POST"
	MethodPut     = "PUT"
	MethodPatch   = "PATCH" // RFC 5789
	MethodDelete  = "DELETE"
	MethodConnect = "CONNECT"
	MethodOptions = "OPTIONS"
	MethodTrace   = "TRACE"
)

Post方法、Put方法、Delete方法等方法都和上面的类似,以Put方法为例

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
)
func main() 
	req, err := http.NewRequest(http.MethodPut, "http://httpbin.org/put", nil)
	if err != nil 
		panic(err)
	
	resp, err := http.DefaultClient.Do(req)
	if err != nil 
		panic(err)
	
	defer resp.Body.Close()
	respData, err := ioutil.ReadAll(resp.Body)
	if err != nil 
		panic(err)
	
	fmt.Printf("%s", respData)

可以发现,只有在创建请求对象时的参数变化外,其它的都没有变。懂了基本的创建之后,再看看包装的。

包装后的Get方法

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
)
func main() 
    // get请求
	req, err := http.Get("http://httpbin.org/get")
	if err != nil 
		panic(err)
	
	defer req.Body.Close()
    // 读取响应数据
	respData, err := ioutil.ReadAll(req.Body)
	if err != nil 
		panic(err)
	
	fmt.Printf("%s", respData)

可以发现其实包装后的,其实就是一个Get方法包装了http.NewRequest( )和http.DefaultClient.Do( )两个方法,因此可推测出包装后的Post方法和包装后的Get方法类似,只需要该一下名字就行。但是还需要注意一点,get请求时,传的参数是放在url中的,而post请求的参数是在Body中的。对于包装的Get方法,只有一个传入参数,但包装后的Post方法有三个传入参数。Post方法签名为:

func http.Post(url string, contentType string, body io.Reader) (resp *http.Response, err error)

如果没有要传递的参数或数据,我们只需要设置contentType为空字符串,body为nil,也可完成请求。

传递时有参数或数据

上面我们都是简单的使用请求方法,而没有传递参数或者数据,下面开始传递参数或者数据。

在学习传递参数之前,先了解一下net/url包的使用,只要我们设置好参数的键和值,它就能帮我们装换成http协议中的参数写法,如下:

package main

import (
	"fmt"
	"net/url"
)

func main() 
	params := make(url.Values)
	params.Add("name", "zhong")
	params.Add("age", "18")
	fmt.Println(params.Encode()

// age=18&name=zhong

你可能疑问我直接写结果的样式只需要几个单词就可以了,为什么还要写这么多代码?

我们的参数中一般都是变量,像结果那样输出的话只需使用字符拼接的方法,很快就写出来了,没那么多代码。但是,在工程代码中,我们看中的是可维护性,也可以说是可读性和可修改性,如果参数很多,那么拼接后就会很长很乱,没有上面写的那么好的可读性。

URL中添加参数

在此之前还需要了解一个net/http包中URL的参数的连接函数,http包下的Request结构体嵌套了URL结构体,URL结构体下的成员变量RawQuery,代表了URL的参数。发起请求时,会自动在请求路径后加上?号,再加上参数。

package main

import (
	"fmt"
	"net/http"
)
func main() 
	req, err := http.NewRequest(http.MethodGet, "http://httpbin.org/get", nil)
	if err != nil 
		return
	
	fmt.Println(req.URL)
	req.URL.RawQuery = "age=18&name=zhong"
	fmt.Println(req.URL)

// http://httpbin.org/get
// http://httpbin.org/get?age=18&name=zhong

所以最终的代码为

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
)
// 设置参数结构并返回结果
func param() string 
	params := make(url.Values)
	params.Add("name", "zhong")
	params.Add("age", "18")
	return params.Encode()

func main() 
	req, err := http.NewRequest(http.MethodGet, "http://httpbin.org/get", nil)
	if err != nil 
		panic(err)
	
    // 设置请求的参数
	req.URL.RawQuery = param()
	resp, err := http.DefaultClient.Do(req)
	if err != nil 
		panic(err)
	
	defer resp.Body.Close()
	respData, err := ioutil.ReadAll(resp.Body)
	if err != nil 
		return
	
	fmt.Printf("%s", respData)

这样写之后就可以达到发起get请求,请求链接为:http://httpbin.org/get?age=18&name=zhong

所以,不管是GET还是POST或者是PUT等方法,只要URL中需要使用到参数,就可以这样写

请求头中添加或修改参数

和URL中加参数基本类似,只要修改对应的值即可,这里添加一个请求头,使用到http.Request.Header.Add( )函数

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
)

func main() 
	req, err := http.NewRequest(http.MethodGet, "http://httpbin.org/get", nil)
	if err != nil 
		panic(err)
	
    // 设置请求头参数
	req.Header.Add("user-agent", "chrome666")
	resp, err := http.DefaultClient.Do(req)
	if err != nil 
		panic(err)
	
	defer resp.Body.Close()
	respData, err := ioutil.ReadAll(resp.Body)
	if err != nil 
		return
	
	fmt.Printf("%s", respData)

这里是添加请求头,如果是修改呢?只需要把Add函数换成Set函数即可。哪这两个有什么区别?

如果你使用Add方法添加一个 k - V 键值对请求头后,再次使用Add方法添加一个相同的K,但V不同,结果就是只有第一个K添加成功,第二个设置失败。

如果使用Set方法设置值的时候,上面的请况就是最后一个设置成功,而且Set函数也可以达到Add函数添加请求头的功能

请求体(body)中加参数

前面也提过,只是默认为nil,就是http.NewRequest( )函数的第三个参数,该参数接收的就是要传递的body数据,该参数的类型是io.Reader,也就是Reader参数。在body中添加参数,最常用的是Post方法,代码就以Post方法的写法为例:

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"strings"
)

func param() *strings.Reader 
	params := make(url.Values)
	params.Add("name", "zhong")
	params.Add("age", "18")
	return strings.NewReader(params.Encode())

func main() 
	req, err := http.NewRequest(http.MethodPost, "http://httpbin.org/post", param())
	if err != nil 
		panic(err)
	
	resp, err := http.DefaultClient.Do(req)
	if err != nil 
		panic(err)
	
	defer resp.Body.Close()
	respData, err := ioutil.ReadAll(resp.Body)
	if err != nil 
		return
	
	fmt.Printf("%s", respData)

和URL中添加参数的写法差不多,只是body的数据要在创建Request对象的时候要指定,且接收的是Reader对象,因此在param函数中将字符串类型的参数转换成Reader对象。

获取响应信息

之前我们输出的都是响应包的body信息,没有输出过其它信息,如状态码、cookie、header等。如下图有这么多可选,但是他们的使用方法却不同

如Body是一个io.ReadCloser类型,因此需要使用ioutil.ReadAll( )方法获取里面的数据。

而Status是string类型、StatusCode是int类型,像这种可以直接输出

Header是一个map[string][]string类型,直接输出的话就输出全部,如果需要指定某一个就可以使用Header[“xxxx”]或者Header.Get(“xxxx”)方法取出响应头xxxx参数

文件下载

平常常下载图片、视频、压缩包等文件,一般都是使用Get请求。以下载图片为例:

package main

import (
	"io"
	"os"
	"net/http"
)

func download(url, filename string) 
    // 发起请求,下载图片数据
	resp, err := http.Get(url)
	if err != nil 
		panic(err)
	
    // 创建文件
	f, err := os.Create(filename)
	if err != nil 
		panic(err)
	
    // 将下载的图片数据写入到文件中
	_, err = io.Copy(f, resp.Body)
	if err != nil 
		panic(err)
	

func main() 
	url := "http://i2.hdslb.com/bfs/face/11a11301fa3d0b4b3430ffeffa1f872ed9a933e1.jpg@128w_128h_1o.webp"
	filename := "touxiang.png"
	download(url, filename)

下载带进度条

在这之前你得了解一些知识

http.Response.ContentLength请求头能得知下载文件的大小

io.Reader对象其实是一个接口,原型如下:

type Reader interface 
	Read(p []byte) (n int, err error)

那么像前面那样使用io.Copy(dst io.Writer, src io.Reader)函数时,对io.Reader做了什么?

io.Copy( )函数会创建一个字节类型的切片(buf = make([]byte, size)),然后循环调用Reader对象下的Read( )方法(nr, er := src.Read(buf)), 将该字节切片数据写入到io.Writer对象中

懂得以上知识后,我们开始设计进度条的一些细节。

首先我们自己设计一个Reader对象,当然我们不可能亲自实现Read方法,我们可以使用继承的方式继承io.Reader对象,然后还可以对自己设计的Reader对象进行功能的扩充,比如增加一些变量。

增加哪些变量呢?

一个进度条是百分比的,所以要知道总文件长度和正在读取文件的长度,这样就可以计算出百分比了,所以我们就增加这两个变量,总长度由http.Response.ContentLength可知,而正在读取文件的长度可以由Read方法的返回值可知。

package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

type Reader struct 
	io.Reader     // 继承
	AllLength int // 文件的总长度
	NowLength int // 正在读取文件的长度


var printchar string = "#"

// os.Copy()函数会不断地调用该方法
func (r *Reader) Read(p []byte) (n int, err error) 
	// 我们不会实现Read()方法,所以再执行一次Reader对象中的Read方法
	// 每次读取http.Response.Body中的数据,就会返回读取的长度
	n, err = r.Reader.Read(p)
	// 由于网络延迟,不是一次性下载完成,所以要统计读取过的总长度
	r.NowLength += n
	// 每次刷新屏幕时就增加一个#号
	printchar += "#"
	// 由于整数除以整数得到的是整数,所以读取过的总长度乘100再除以文件总长度
	fmt.Printf("[%-100s] %d %%\\r", printchar, r.NowLength*100/r.AllLength)
	return n, err


func download(url, filename string) 
	resp, err := http.Get(url)
	if err != nil 
		panic(err)
	
	f, err := os.Create(filename)
	if err != nil 
		panic(err)
	
	// 初始化Reader对象。把我们要写入的数据赋给io.Reader
	reader := &Reader
		Reader:    resp.Body,
		AllLength: int(resp.ContentLength),
	
	// Copy的io.Reader参数换成了自己设计的Reader对象
	_, err = io.Copy(f, reader)
	if err != nil 
		panic(err)
	

func main() 
	url := "https://vd2.bdstatic.com/mda-madjt1deuj2r0n97/sc/cae_h264_clips/1610607610/mda-madjt1deuj2r0n97.mp4?auth_key=1621783864-0-0-ca2c58d1b1c1f0e53e433b2227bbf19f&bcevod_channel=searchbox_feed&pd=1&pt=3&abtest="
	filename := "shipin.mp4"
	download(url, filename)
	fmt.Println("\\n")

body数据的格式

在请求的body中提交数据时,可以提交多种格式的数据。主要是form、json、文件这三种格式

form格式

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"strings"
)

func main() 
	params := make(url.Values)
	params.Add("name", "zhong")
	params.Add("age", "18")
	resp, err := http.Post(
		"http://httpbin.org/post",
		"application/x-www-form-urlencoded",
		strings.NewReader(params.Encode()))
	if err != nil 
		panic(err)
	
	defer resp.Body.Close()
	respData, err := ioutil.ReadAll(resp.Body)
	if err != nil 
		return
	
	fmt.Printf("%s", respData)

json格式

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
)

type ParamJson struct 
	Name string `json:"name"`
	Age  int    `json:"age"`


func main() 
	paramJson := &ParamJson
		Name: "zhong",
		Age:  18,
	
    // 结构体转为json格式,返回的是字节切片
	paramjson, _ := json.Marshal(paramJson)
	resp, err := http.Post(
		"http://httpbin.org/post",
		"application/json",
		bytes.NewReader(paramjson))  // 将字节切片转为Reader对象

	if err != nil 
		panic(err)
	
	defer resp.Body.Close()
	respData, err := ioutil.ReadAll(resp.Body)
	if err != nil 
		return
	
	fmt.Printf("%s", respData)

以上是关于Golang net/http客户端的使用的主要内容,如果未能解决你的问题,请参考以下文章

Go语言基础之net/http

golang 简单web服务

『GCTT 出品』用不到 100 行的 Golang 代码实现 HTTP(S) 代理

golang搭建web服务器

深入理解Golang之http server

PubSub Golang 客户端的高发布延迟