客户端
快速开始
package main
import (
"context"
"fmt"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/client"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/cloudwego/hertz/pkg/protocol"
"github.com/cloudwego/hertz/pkg/protocol/consts"
)
func performRequest() {
c, _ := client.NewClient()
req, resp := protocol.AcquireRequest(), protocol.AcquireResponse()
req.SetRequestURI("http://localhost:8080/hello")
req.SetMethod("GET")
_ = c.Do(context.Background(), req, resp)
fmt.Printf("get response: %s\n", resp.Body()) // status == 200 resp.Body() == []byte("hello hertz")
}
func main() {
h := server.New(server.WithHostPorts(":8080"))
h.GET("/hello", func(c context.Context, ctx *app.RequestContext) {
ctx.JSON(consts.StatusOK, "hello hertz")
})
go performRequest()
h.Spin()
}
配置
配置项 | 默认值 | 描述 |
---|---|---|
DialTimeout | 1s | 拨号超时时间 |
MaxConnsPerHost | 512 | 每个主机可能建立的最大连接数 |
MaxIdleConnDuration | 10s | 最大的空闲连接持续时间,空闲的连接在此持续时间后被关闭 |
MaxConnDuration | 0s | 最大的连接持续时间,keep-alive 连接在此持续时间后被关闭 |
MaxConnWaitTimeout | 0s | 等待空闲连接的最大时间 |
KeepAlive | true | 是否使用 keep-alive 连接,默认使用 |
ReadTimeout | 0s | 完整读取响应(包括 body)的最大持续时间 |
TLSConfig | nil | 设置用于创建 tls 连接的 tlsConfig,具体配置信息请看 tls-config |
Dialer | network.Dialer | 设置指定的拨号器 |
ResponseBodyStream | false | 是否在流中读取 body,默认不在流中读取 |
DisableHeaderNamesNormalizing | false | 是否禁用头名称规范化,默认不禁用,如 cONTENT-lenGTH -> Content-Length |
Name | "" | 用户代理头中使用的客户端名称 |
NoDefaultUserAgentHeader | false | 是否没有默认的 User-Agent 头,默认有 User-Agent 头 |
DisablePathNormalizing | false | 是否禁用路径规范化,默认规范路径,如 http://localhost:8080/hello/../ hello -> http://localhost:8080/hello |
RetryConfig | nil | HTTP 客户端的重试配置,重试配置详细说明请看 重试 |
WriteTimeout | 0s | HTTP 客户端的写入超时时间 |
HostClientStateObserve | nil | 观察和记录 HTTP 客户端的连接状态的函数 |
ObservationInterval | 5s | HTTP 客户端连接状态的观察执行间隔 |
DialFunc | network.Dialer | 设置 HTTP 客户端拨号器函数,会覆盖自定义拨号器 |
示例代码:
func main() {
observeInterval := 10 * time.Second
stateFunc := func(state config.HostClientState) {
fmt.Printf("state=%v\n", state.ConnPoolState().Addr)
}
var customDialFunc network.DialFunc = func(addr string) (network.Conn, error) {
return nil, nil
}
c, err := client.NewClient(
client.WithDialTimeout(1*time.Second),
client.WithMaxConnsPerHost(1024),
client.WithMaxIdleConnDuration(10*time.Second),
client.WithMaxConnDuration(10*time.Second),
client.WithMaxConnWaitTimeout(10*time.Second),
client.WithKeepAlive(true),
client.WithClientReadTimeout(10*time.Second),
client.WithDialer(standard.NewDialer()),
client.WithResponseBodyStream(true),
client.WithDisableHeaderNamesNormalizing(true),
client.WithName("my-client"),
client.WithNoDefaultUserAgentHeader(true),
client.WithDisablePathNormalizing(true),
client.WithRetryConfig(
retry.WithMaxAttemptTimes(3),
retry.WithInitDelay(1000),
retry.WithMaxDelay(10000),
retry.WithDelayPolicy(retry.DefaultDelayPolicy),
retry.WithMaxJitter(1000),
),
client.WithWriteTimeout(10*time.Second),
client.WithConnStateObserve(stateFunc, observeInterval),
client.WithDialFunc(customDialFunc, netpoll.NewDialer()),
)
if err != nil {
return
}
status, body, _ := c.Get(context.Background(), nil, "http://www.example.com")
fmt.Printf("status=%v body=%v\n", status, string(body))
}
发送请求
func (c *Client) Do(ctx context.Context, req *protocol.Request, resp *protocol.Response) error
func (c *Client) DoRedirects(ctx context.Context, req *protocol.Request, resp *protocol.Response, maxRedirectsCount int) error
func (c *Client) Get(ctx context.Context, dst []byte, url string, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error)
func (c *Client) Post(ctx context.Context, dst []byte, url string, postArgs *protocol.Args, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error)
Do
Do 函数执行给定的 http 请求并填充给定的 http 响应。请求必须包含至少一个非零的 RequestURI,其中包含完整的 URL 或非零的 Host header + RequestURI。
该函数不会跟随重定向,请使用 Get 函数或 DoRedirects 函数或 Post 函数来跟随重定向。
如果 resp 为 nil,则会忽略响应。如果所有针对请求主机的 DefaultMaxConnsPerHost 连接都已忙,则会返回 ErrNoFreeConns
错误。在性能关键的代码中,建议通过 AcquireRequest 和 AcquireResponse 获取 req 和 resp。
函数签名:
func (c *Client) Do(ctx context.Context, req *protocol.Request, resp *protocol.Response) error
示例代码:
func main() {
// hertz server:http://localhost:8080/ping ctx.String(consts.StatusOK, "pong")
c, err := client.NewClient()
if err != nil {
return
}
req, res := &protocol.Request{}, &protocol.Response{}
req.SetMethod(consts.MethodGet)
req.SetRequestURI("http://localhost:8080/ping")
err = c.Do(context.Background(), req, res)
fmt.Printf("resp = %v,err = %+v", string(res.Body()), err)
// resp.Body() == []byte("pong") err == <nil>
}
DoRedirects
DoRedirects 函数执行给定的 http 请求并填充给定的 http 响应,遵循最多 maxRedirectsCount 次重定向。当重定向次数超过 maxRedirectsCount 时,将返回 ErrTooManyRedirects
错误。
函数签名:
func (c *Client) DoRedirects(ctx context.Context, req *protocol.Request, resp *protocol.Response, maxRedirectsCount int) error
示例代码:
func main() {
// hertz server
// http://localhost:8080/redirect ctx.Redirect(consts.StatusMovedPermanently, []byte("/redirect2"))
// http://localhost:8080/redirect2 ctx.Redirect(consts.StatusMovedPermanently, []byte("/redirect3"))
// http://localhost:8080/redirect3 ctx.String(consts.StatusOK, "pong")
c, err := client.NewClient()
if err != nil {
return
}
req, res := &protocol.Request{}, &protocol.Response{}
req.SetMethod(consts.MethodGet)
req.SetRequestURI("http://localhost:8080/redirect")
err = c.DoRedirects(context.Background(), req, res, 1)
fmt.Printf("resp = %v,err = %+v\n", string(res.Body()), err)
// res.Body() == []byte("") err.Error() == "too many redirects detected when doing the request"
err = c.DoRedirects(context.Background(), req, res, 2)
fmt.Printf("resp = %v,err = %+v\n", string(res.Body()), err)
// res.Body() == []byte("pong") err == <nil>
}
Get
Get 函数返回 URL 的状态码和响应体。如果 dst 太小,则将被响应体替换并返回,否则将分配一个新的切片。
该函数会自动跟随重定向。
函数签名:
func (c *Client) Get(ctx context.Context, dst []byte, url string, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error)
示例代码:
func main() {
// hertz server:http://localhost:8080/ping ctx.String(consts.StatusOK, "pong")
c, err := client.NewClient()
if err != nil {
return
}
status, body, err := c.Get(context.Background(), nil, "http://localhost:8080/ping")
fmt.Printf("status=%v body=%v err=%v\n", status, string(body), err)
// status == 200 res.Body() == []byte("pong") err == <nil>
}
Post
Post 函数使用给定的 POST 参数向指定的 URL 发送 POST 请求。如果 dst 太小,则将被响应体替换并返回,否则将分配一个新的切片。
该函数会自动跟随重定向。
如果 postArgs 为 nil ,则发送空的 POST 请求体。
函数签名:
func (c *Client) Post(ctx context.Context, dst []byte, url string, postArgs *protocol.Args, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error)
示例代码:
func main() {
// hertz server:http://localhost:8080/hello ctx.String(consts.StatusOK, "hello %s", ctx.PostForm("name"))
c, err := client.NewClient()
if err != nil {
return
}
var postArgs protocol.Args
postArgs.Set("name", "cloudwego") // Set post args
status, body, err := c.Post(context.Background(), nil, "http://localhost:8080/hello", &postArgs)
fmt.Printf("status=%v body=%v err=%v\n", status, string(body), err)
// status == 200 res.Body() == []byte("hello cloudwego") err == <nil>
}
请求超时
func (c *Client) DoTimeout(ctx context.Context, req *protocol.Request, resp *protocol.Response, timeout time.Duration) error
func (c *Client) DoDeadline(ctx context.Context, req *protocol.Request, resp *protocol.Response, deadline time.Time) error
func (c *Client) GetTimeout(ctx context.Context, dst []byte, url string, timeout time.Duration, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error)
func (c *Client) GetDeadline(ctx context.Context, dst []byte, url string, deadline time.Time, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error)
DoTimeout
DoTimeout 函数执行给定的请求并在给定的超时时间内等待响应。
该函数不会跟随重定向,请使用 Get 函数或 DoRedirects 函数或 Post 函数来跟随重定向。
如果 resp 为 nil,则会忽略响应。如果在给定的超时时间内未能收到响应,则会返回 errTimeout
错误。
函数签名:
func (c *Client) DoTimeout(ctx context.Context, req *protocol.Request, resp *protocol.Response, timeout time.Duration) error
示例代码:
func main() {
// hertz server:http://localhost:8080/ping ctx.String(consts.StatusOK, "pong")
c, err := client.NewClient()
if err != nil {
return
}
req, res := &protocol.Request{}, &protocol.Response{}
req.SetMethod(consts.MethodGet)
req.SetRequestURI("http://localhost:8080/ping")
err = c.DoTimeout(context.Background(), req, res, time.Second*3)
fmt.Printf("resp = %v,err = %+v\n", string(res.Body()), err)
// res.Body() == []byte("pong") err == <nil>
err = c.DoTimeout(context.Background(), req, res, time.Second)
fmt.Printf("resp = %v,err = %+v\n", string(res.Body()), err)
// res.Body() == []byte("") err.Error() == "timeout"
}
DoDeadline
DoDeadline 执行给定的请求并等待响应,直至给定的最后期限。
该函数不会跟随重定向,请使用 Get 函数或 DoRedirects 函数或 Post 函数来跟随重定向。
如果 resp 为 nil,则会忽略响应。如果在给定的截止日期之前未能收到响应,则会返回 errTimeout
错误。
函数签名:
func (c *Client) DoDeadline(ctx context.Context, req *protocol.Request, resp *protocol.Response, deadline time.Time) error
示例代码:
func main() {
// hertz server:http://localhost:8080/ping ctx.String(consts.StatusOK, "pong")
c, err := client.NewClient()
if err != nil {
return
}
req, res := &protocol.Request{}, &protocol.Response{}
req.SetMethod(consts.MethodGet)
req.SetRequestURI("http://localhost:8080/ping")
err = c.DoDeadline(context.Background(), req, res, time.Now().Add(3*time.Second))
fmt.Printf("resp = %v,err = %+v\n", string(res.Body()), err)
// res.Body() == []byte("pong") err == <nil>
err = c.DoDeadline(context.Background(), req, res, time.Now().Add(1*time.Second))
fmt.Printf("resp = %v,err = %+v\n", string(res.Body()), err)
// res.Body() == []byte("") err.Error() == "timeout"
}
GetTimeOut
GetTimeout 函数返回 URL 的状态码和响应体。如果 dst 太小,则将被响应体替换并返回,否则将分配一个新的切片。
该函数会自动跟随重定向。
如果在给定的超时时间内无法获取 URL 的内容,则会返回 errTimeout
错误。
注意:GetTimeout 不会终止请求本身。该请求将在后台继续进行,并且响应将被丢弃。如果请求时间过长且连接池已满,请尝试使用具有 ReadTimeout 配置的自定义 Client 实例或像下面这样设置请求级别的读取超时时间:
codeGetTimeout(ctx, dst, url, timeout, config.WithReadTimeout(1 * time.Second))
函数签名:
func (c *Client) GetTimeout(ctx context.Context, dst []byte, url string, timeout time.Duration, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error)
示例代码:
func main() {
// hertz server:http://localhost:8080/ping ctx.String(consts.StatusOK, "pong")
c, err := client.NewClient()
if err != nil {
return
}
status, body, err := c.GetTimeout(context.Background(), nil, "http://localhost:8080/ping", 3*time.Second)
fmt.Printf("status=%v body=%v err=%v\n", status, string(body), err)
// status == 200 res.Body() == []byte("pong") err == <nil>
status, body, err = c.GetTimeout(context.Background(), nil, "http://localhost:8080/ping", 1*time.Second)
fmt.Printf("status=%v body=%v err=%v\n", status, string(body), err)
// status == 0 res.Body() == []byte("") err.Error() == "timeout"
}
GetDeadline
GetDeadline 函数返回 URL 的状态码和响应体。如果 dst 太小,则将被响应体替换并返回,否则将分配一个新的切片。
该函数会自动跟随重定向。
如果在给定的截止时间之前无法获取 URL 的内容,则会返回 errTimeout
错误。
注意:GetDeadline 不会终止请求本身。该请求将在后台继续进行,并且响应将被丢弃。如果请求时间过长且连接池已满,请尝试使用具有ReadTimeout 配置的自定义 Client 实例或像下面这样设置请求级别的读取超时时间:
GetDeadline(ctx, dst, url, deadline, config.WithReadTimeout(1 * time.Second))
函数签名:
func (c *Client) GetDeadline(ctx context.Context, dst []byte, url string, deadline time.Time, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error)
示例代码:
func main() {
// hertz server:http://localhost:8080/ping ctx.String(consts.StatusOK, "pong")
c, err := client.NewClient()
if err != nil {
return
}
status, body, err := c.GetDeadline(context.Background(), nil, "http://localhost:8080/ping", time.Now().Add(3*time.Second))
fmt.Printf("status=%v body=%v err=%v\n", status, string(body), err)
// status == 200 res.Body() == []byte("pong") err == <nil>
status, body, err = c.GetDeadline(context.Background(), nil, "http://localhost:8080/ping", time.Now().Add(time.Second))
fmt.Printf("status=%v body=%v err=%v\n", status, string(body), err)
// status == 0 res.Body() == []byte("") err.Error() == "timeout"
}
请求重试
func (c *Client) SetRetryIfFunc(retryIf client.RetryIfFunc)
SetRetryIfFunc
SetRetryIfFunc
方法用于自定义配置重试发生的条件。(更多内容请参考 retry-条件配置)
函数签名:
func (c *Client) SetRetryIfFunc(retryIf client.RetryIfFunc)
示例代码:
func main() {
c, err := client.NewClient()
if err != nil {
return
}
var customRetryIfFunc = func(req *protocol.Request, resp *protocol.Response, err error) bool {
return true
}
c.SetRetryIfFunc(customRetryIfFunc)
status2, body2, _ := c.Get(context.Background(), nil, "http://www.example.com")
fmt.Printf("status=%v body=%v\n", status2, string(body2))
}
添加请求内容
Hertz 客户端可以在 HTTP 请求中添加 query
参数、www-url-encoded
、multipart/form-data
、json
等多种形式的请求内容。
示例代码:
func main() {
client, err := client.NewClient()
if err != nil {
return
}
req := &protocol.Request{}
res := &protocol.Response{}
// Use SetQueryString to set query parameters
req.Reset()
req.Header.SetMethod(consts.MethodPost)
req.SetRequestURI("http://127.0.0.1:8080/v1/bind")
req.SetQueryString("query=query&q=q1&q=q2&vd=1")
err = client.Do(context.Background(), req, res)
if err != nil {
return
}
// Send "www-url-encoded" request
req.Reset()
req.Header.SetMethod(consts.MethodPost)
req.SetRequestURI("http://127.0.0.1:8080/v1/bind?query=query&q=q1&q=q2&vd=1")
req.SetFormData(map[string]string{
"form": "test form",
})
err = client.Do(context.Background(), req, res)
if err != nil {
return
}
// Send "multipart/form-data" request
req.Reset()
req.Header.SetMethod(consts.MethodPost)
req.SetRequestURI("http://127.0.0.1:8080/v1/bind?query=query&q=q1&q=q2&vd=1")
req.SetMultipartFormData(map[string]string{
"form": "test form",
})
err = client.Do(context.Background(), req, res)
if err != nil {
return
}
// Send "Json" request
req.Reset()
req.Header.SetMethod(consts.MethodPost)
req.Header.SetContentTypeBytes([]byte("application/json"))
req.SetRequestURI("http://127.0.0.1:8080/v1/bind?query=query&q=q1&q=q2&vd=1")
data := struct {
Json string `json:"json"`
}{
"test json",
}
jsonByte, _ := json.Marshal(data)
req.SetBody(jsonByte)
err = client.Do(context.Background(), req, res)
if err != nil {
return
}
}
上传文件
Hertz 客户端支持向服务器上传文件。
示例代码:
func main() {
client, err := client.NewClient()
if err != nil {
return
}
req := &protocol.Request{}
res := &protocol.Response{}
req.SetMethod(consts.MethodPost)
req.SetRequestURI("http://127.0.0.1:8080/singleFile")
req.SetFile("file", "your file path")
err = client.Do(context.Background(), req, res)
if err != nil {
return
}
fmt.Println(err, string(res.Body()))
}
流式读响应内容
Hertz 客户端支持流式读取 HTTP 响应内容。(更多内容请参考 Client)
示例代码:
func main() {
c, _ := client.NewClient(client.WithResponseBodyStream(true))
req := &protocol.Request{}
resp := &protocol.Response{}
req.SetMethod(consts.MethodGet)
req.SetRequestURI("http://127.0.0.1:8080/streamWrite")
err := c.Do(context.Background(), req, resp)
if err != nil {
return
}
bodyStream := resp.BodyStream()
p := make([]byte, resp.Header.ContentLength()/2)
_, err = bodyStream.Read(p)
if err != nil {
fmt.Println(err.Error())
}
left, _ := ioutil.ReadAll(bodyStream)
fmt.Println(string(p), string(left))
}
服务发现
Hertz 客户端支持通过服务发现寻找目标服务器。
Hertz 支持自定义服务发现模块,更多内容可参考 服务发现拓展。
Hertz 目前已接入的服务发现中心相关内容可参考 服务注册与发现。
负载均衡
Hertz 客户端默认提供了 WeightedRandom
负载均衡实现,同时也支持自定义负载均衡实现,更多内容可参考 负载均衡拓展。
TLS
Hertz 客户端默认使用的网络库 netpoll 不支持 TLS,如果要配置 TLS 访问 https 地址,应该使用标准库。
TLS 相关的配置信息可参考 tls-config。
示例代码:
func main() {
clientCfg := &tls.Config{
InsecureSkipVerify: true,
}
c, err := client.NewClient(
client.WithTLSConfig(clientCfg),
client.WithDialer(standard.NewDialer()),
)
if err != nil {
return
}
req, res := &protocol.Request{}, &protocol.Response{}
req.SetMethod(consts.MethodGet)
req.SetRequestURI("https://www.example.com")
err = c.Do(context.Background(), req, res)
fmt.Printf("resp = %v,err = %+v", string(res.Body()), err)
}
正向代理
func (c *Client) SetProxy(p protocol.Proxy)
SetProxy
SetProxy 用来设置客户端代理。(更多内容请参考 正向代理)
注意:同一个客户端不能设置多个代理,如果需要使用另一个代理,请创建另一个客户端并为其设置代理。
示例代码:
func (c *Client) SetProxy(p protocol.Proxy)
函数签名:
func main() {
// Proxy address
proxyURL := "http://<__user_name__>:<__password__>@<__proxy_addr__>:<__proxy_port__>"
parsedProxyURL := protocol.ParseURI(proxyURL)
client, err := client.NewClient(client.WithDialer(standard.NewDialer()))
if err != nil {
return
}
client.SetProxy(protocol.ProxyURI(parsedProxyURL))
upstreamURL := "http://google.com"
_, body, _ := client.Get(context.Background(), nil, upstreamURL)
fmt.Println(string(body))
}
设置客户端工厂对象
func (c *Client) SetClientFactory(cf suite.ClientFactory)
SetClientFactory
SetClientFactory
方法用于设置客户端工厂对象,该工厂对象用于创建 HTTP 客户端对象。
函数签名:
func (c *Client) SetClientFactory(cf suite.ClientFactory)
示例代码:
func main() {
c, err := client.NewClient()
if err != nil {
return
}
c.SetClientFactory(factory.NewClientFactory(nil))
status, body, _ := c.Get(context.Background(), nil, "http://www.example.com")
fmt.Printf("status=%v body=%v\n", status, string(body))
}
关闭空闲连接
func (c *Client) CloseIdleConnections()
CloseIdleConnections
CloseIdleConnections
方法用于关闭任何处于空闲状态的 keep-alive
连接。这些连接可能是之前的请求所建立的,但现在已经空闲了一段时间。该方法不会中断任何当前正在使用的连接。
函数签名:
func (c *Client) CloseIdleConnections()
示例代码:
func main() {
c, err := client.NewClient()
if err != nil {
return
}
status, body, _ := c.Get(context.Background(), nil, "http://www.example.com")
fmt.Printf("status=%v body=%v\n", status, string(body))
// close idle connections
c.CloseIdleConnections()
}
获取拨号器名称
func (c *Client) GetDialerName() (dName string, err error)
GetDialerName
GetDialerName
方法用于获取客户端当前使用的拨号器的名称。如果无法获取拨号器名称,则返回 unknown
。
函数签名:
func (c *Client) GetDialerName() (dName string, err error)
示例代码:
func main() {
c, err := client.NewClient()
if err != nil {
return
}
// get dialer name
dName, err := c.GetDialerName()
if err != nil {
fmt.Printf("GetDialerName failed: %v", err)
return
}
fmt.Printf("dialer name=%v\n", dName)
// dName == "standard"
}
中间件
func (c *Client) Use(mws ...Middleware)
func (c *Client) UseAsLast(mw Middleware) error
func (c *Client) TakeOutLastMiddleware() Middleware
Use
使用 Use
方法对当前 client 增加一个中间件。(更多内容请参考 客户端中间件)
函数签名:
func (c *Client) Use(mws ...Middleware)
UseAsLast
UseAsLast
函数将中间件添加到客户端中间件链的最后。
如果客户端中间件链在之前已经设置了最后一个中间件,UseAsLast
函数将会返回 errorLastMiddlewareExist
错误。因此,为确保客户端中间件链的最后一个中间件为空,可以先使用 TakeOutLastMiddleware 函数清空客户端中间件链的最后一个中间件。
注意:
UseAsLast
函数将中间件设置在了c.lastMiddleware
中,而使用Use 函数设置的中间件链存放在c.mws
中,两者相对独立,只是在执行客户端中间件链的最后才执行c.lastMiddleware
,因此UseAsLast
函数在 Use 函数之前或之后调用皆可。
函数签名:
func (c *Client) UseAsLast(mw Middleware) error
示例代码:
func main() {
client, err := client.NewClient()
if err != nil {
return
}
client.Use(MyMiddleware)
client.UseAsLast(LastMiddleware)
req := &protocol.Request{}
res := &protocol.Response{}
req.SetRequestURI("http://www.example.com")
err = client.Do(context.Background(), req, res)
if err != nil {
return
}
}
TakeOutLastMiddleware
TakeOutLastMiddleware
函数返回 UseAsLast 函数中设置的最后一个中间件并将其清空,若没有设置则返回 nil
。
函数签名:
func (c *Client) TakeOutLastMiddleware() Middleware
示例代码:
func main() {
client, err := client.NewClient()
if err != nil {
return
}
client.Use(MyMiddleware)
client.UseAsLast(LastMiddleware)
req := &protocol.Request{}
res := &protocol.Response{}
req.SetRequestURI("http://www.example.com")
err = client.Do(context.Background(), req, res)
if err != nil {
return
}
middleware := client.TakeOutLastMiddleware() // middleware == LastMiddleware
middleware = client.TakeOutLastMiddleware() // middleware == nil
}