fetch

package module
v0.1.10 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 20, 2021 License: MIT Imports: 13 Imported by: 1

README

fetch

http client 网络请求封装

Overview

涵盖功能:

  • 支持 Get/Post/Put/Delete/Head 等方法
  • 支持 Path 参数设置
  • 支持自定义设置 client
  • 支持 debug 模式打印请求和响应详细
  • 支持 timeout 超时和 ctx 超时设置
  • 支持自定义 Interceptor 拦截器设置
  • 支持自定义 Bind 解析请求响应

Contents

Installation

  • install fetch
go get -u github.com/beanscc/fetch
  • import it in your code
import "github.com/beanscc/fetch"

Quick Start

// github.com/beanscc/fetch/examples/basic/main.go
package main

import (
	"context"
	"encoding/json"
	"log"
	"net/http"
	"net/http/httptest"

	"github.com/beanscc/fetch"
	"github.com/beanscc/fetch/body"
)

func main() {
	type Resp struct {
		Name   string `json:"name"`
		Age    uint8  `json:"age"`
		Addr   string `json:"address"`
		Mobile string `json:"mobile"`
	}

	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		out := baseResp{
			Code: 0,
			Msg:  "ok",
			Data: &Resp{
				Name:   "ming.liu",
				Age:    20,
				Addr:   "beijing wangfujing street",
				Mobile: "+86-13800000000",
			},
		}

		res, _ := json.Marshal(out)
		w.Header().Set("content-type", body.MIMEJSON)
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write(res)
	}))

	// var data Resp
	// res := newBaseResp(&data)
	// err := fetch.Get(context.Background(), ts.URL+"/api/user").
	// 	Query("id", 10).
	// 	BindJSON(&res)

	// OR
	f := fetch.New(ts.URL, &fetch.Options{
		Debug: true,
		Interceptors: []fetch.Interceptor{
			// fetch.LogInterceptor 会输出请求和响应日志
			fetch.LogInterceptor(&fetch.LogInterceptorRequest{
				MaxReqBody: 5,
				// MaxRespBody: 10,
				Logger: func(ctx context.Context, format string, args ...interface{}) {
					v1, _ := ctx.Value("k1").(string)
					allArgs := []interface{}{v1}
					allArgs = append(allArgs, args...)
					log.Printf("extra k1:%v, "+format, allArgs...)
				},
			}),
		},
	})

	ctx := context.WithValue(context.Background(), "k1", "v1")

	var data Resp
	res := newBaseResp(&data)
	err := f.Post(ctx, "api/user").
		AddHeader("hk_1", "hk_1_val").
		AddHeader(map[string]interface{}{
			"hk_2": 24,
			"hk_3": "hk_3_val",
		}).
		AddHeader("hk_4", 4, map[string]interface{}{"hk_5": 66.66}, "hk_6", "hk_6_val").
		SetHeader("hk_1", 111).
		Query("id", 10).
		JSON(`{"age": 18}`).
		BindJSON(&res)
	if err != nil {
		log.Printf("fetch.Get() failed. err:%v", err)
		return
	}
	log.Printf("fetch.Get() data:%+v", res.Data) // output: fetch.Get() data:&{Name:ming.liu Age:20 Addr:beijing wangfujing street Mobile:+86-13800000000}

	// output:
	/*
		2020/07/01 16:41:38 [Fetch] GET /api/user?id=10 HTTP/1.1
		Host: 127.0.0.1:50305
		User-Agent: Go-http-client/1.1
		Hk_1: 111
		Hk_2: 24
		Hk_3: hk_3_val
		Hk_4: 4
		Hk_5: 66.66
		Hk_6: hk_6_val
		Accept-Encoding: gzip

		b
		{"age": 18}
		0

		2020/07/01 16:41:38 [Fetch] HTTP/1.1 200 OK
		Content-Length: 122
		Content-Type: application/json
		Date: Wed, 01 Jul 2020 08:41:38 GMT

		{"data":{"name":"ming.liu","age":20,"address":"beijing wangfujing street","mobile":"+86-13800000000"},"code":0,"msg":"ok"}
		2020/07/01 16:41:38 extra k1:v1, [Fetch] method: POST,, url: http://127.0.0.1:50305/api/user?id=10, header: map[Hk_1:[111] Hk_2:[24] Hk_3:[hk_3_val] Hk_4:[4] Hk_5:[66.66] Hk_6:[hk_6_val]], body: '{"age...', latency: 1.038371ms, status: 200, resp: {"data":{"name":"ming.liu","age":20,"address":"beijing wangfujing street","mobile":"+86-13800000000"},"code":0,"msg":"ok"}, err: <nil>
		2020/07/01 16:41:38 fetch.Get() data:&{Name:ming.liu Age:20 Addr:beijing wangfujing street Mobile:+86-13800000000}
	*/
}

type baseResp struct {
	Data interface{} `json:"data,empty"`
	Code int         `json:"code"`
	Msg  string      `json:"msg"`
}

func newBaseResp(data interface{}) *baseResp {
	return &baseResp{
		Data: data,
		Code: 0,
		Msg:  "ok",
	}
}

API Examples

创建 Fetch
// 不指定 Fetch 客户端请求的基础域名,需要在 Get/Post... 等方法时,使用绝对地址
f := fetch.New("")

// 指定基础域名地址, 后面 Get/Post ... 等方法调用时,可使用相对地址,也可以使用绝对地址
f2 := fetch.New("http://api.domain.com")

// 指定基础域名,同时开启 debug 模式(debug 模式,将使用标准包 "log" 以文本格式,输出请求和响应的详细日志)
f3 := fetch.New("http://api.domain.com", fetch.Debug(true))

f4 := fetch.New("", &fetch.Options{
    Debug: true,
    Timeout: 10 * time.Second,
})
Options 的使用
Debug

debug 默认是 false 关闭的,若设置为 true,则为开启状态。debug 开启时,将以标准包 log 文本形式输出请求和响应的信息

f := fetch.New(ts.URL, fetch.Debug(true)).
		Get(context.Background(), "api/user").
		Query("id", 10).
		Bytes()

// output
/*
2020/06/30 01:15:55 [Fetch] GET /api/user?id=10 HTTP/1.1
Host: 127.0.0.1:49893
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip

2020/06/30 01:15:55 [Fetch] HTTP/1.1 200 OK
Content-Length: 119
Content-Type: application/json
Date: Mon, 29 Jun 2020 17:15:55 GMT

{"data":{"addr":"beijing wangfujing street","age":20,"mobile":"+86-13800000000","name":"ming.liu"},"code":0,"msg":"ok"}
*/
Timeout
// Timeout 设置 Fetch 全局超时
f := fetch.New("", fetch.Timeout(10*time.Second))
// 或 设置某次请求的超时时间
f = f.WithOptions(fetch.Timeout(3 * time.Second))

超时控制也可以通过 context 设置超时时间:Timeout 超时控制

Method 设置
// 使用默认 Fetch
f := fetch.Get(context.Background(), "api/user")

// 自定义 Fetch 的基础域名,同时通过 timeout option 设置每次请求的超时时间
f1 := fetch.New("http://api.domain.com/", fetch.Timeout(10 *time.Second))

f1tmp := f1.Get(context.Background, "api/user").
	Query("id",10)
...

f2tmp := f1.Post(context.Background, "api/user").
	JSON(map[string]interface{}{"id": 10, "name": "ming.liu"})
...

f3tmp := f1.Method("Get", "api/user")
...

每个 Method() 都将返回一个新的 *Fetch 对象,该对象包含原 *Fetch 对象属性基础选项属性(ctx, reqerr 等属于一次性请求参数,不在 clone 范围内); 所以,若在 Method() 方法后,进行非链式操作,必须接收 Method() 方法或其链式操作后返回的新 *Fetch 对象

// 错误使用方式
f.Get(ctx, "city")                  // 需要接收 Get() 返回的新 *Fetch 对象,下面的操作才不会出错
b, err := f.Query("id", "1").Text() // err != nil, err="fetch: empty method"

// 正确的方式:将下面的操作组成一个链式操作
b, err := f.Get(ctx, "city").Query("id", "1").Text()
// 或
f = f.Get(ctx, "city")
b, err := f.Query("id", 1).Text()
path 动态参数设置
f := fetch.Get(ctx, "api/user/:uid/address/:address_id", 1, 20)
// 就是请求 api/user/1/address/20

在 path 中定义动态参数,使用 : 表示动态参数,如 api/user/:uid/address/:address_id

调用时,在 Get/Post ... 等请求方法 path 参数后,按顺序加上 path 参数实际的值,即可

Query 设置
f = f.Get(ctx, "api/user")

// query 参数以 key/val 对形式设置(必须成对)
f = f.Query("id", 1).Query("age", 12).Query("name", "ming.liu")

// 或通过 map[string]interface{} 一次设置多个key/val对
f1 := f.Query(map[string]interface{}{
	"id":   1,
	"age": 12,
})

// 或者 key/val 对和 map[string]interface{} 交替形式设置
f2 := f.Query("id", 1, map[string]interface{}{
	"age": 12,
	"name": "ming.liu",
}, "height", 175)
Header 设置
f = f.Get(ctx, "city")

// AddHeader 传参数方式类似 Query 传参
f.AddHeader("hk_1", "hk_1_val").
		AddHeader(map[string]interface{}{
			"hk_2": 24,
			"hk_3": "hk_3_val",
		}).
		AddHeader("hk_4", 4, map[string]interface{}{"hk_5": 66.66}, "hk_6", "hk_6_val")

// SetHeader 和 AddHeader 一样
f.SetHeader("app-time", time.Now().UnixNano()) // 将覆盖上面 "app-time" 的值
Body 设置
// import "github.com/beanscc/fetch/body"

// Body 构造请求的body
type Body interface {
	// Body 构造http请求body
	Body() (io.Reader, error)
	// ContentType 返回 body 体结构相应的 Header content-type 类型
	ContentType() string
}

// Body 接口实现检查
var (
	_ Body = &JSON{}
	_ Body = &XML{}
	_ Body = &Form{}
	_ Body = &MultipartForm{}
)
发送 application/json 数据

Content-Type: "application/json"

f := f.Post(ctx, "api/user")

// 支持 string 类型 json 字符串
f.JSON(`{"name": "alice", "age": 12}`)

// 支持 []byte 类型 json
f.JSON([]byte(`{"name": "alice", "age": 12}`))

// 非 string / []byte 类型,将都调用 json.Marshal 进行序列化
f.JSON(map[string]interface{}{
	"name": "alice",
	"age":  12,
})

type User struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

user := User{Name: "alice", Age: 12}
f.JSON(user)

示例:github.com/beanscc/fetch/fetch_test.go:TestFetchPostJSON

func TestFetchPostJSON(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("content-type", body.MIMEJSON)
		w.Header().Add("x-request-id", fmt.Sprintf("trace-id-%d", time.Now().UnixNano()))
		out := newTestBaseResp(nil)
		fmt.Fprintln(w, out.json())
	}))

	var res testBaseResp
	f := fetch.New(ts.URL, fetch.Debug(true), fetch.Interceptors(
		// fetch.LogInterceptor 会输出以下日志内容
		fetch.LogInterceptor(&fetch.LogInterceptorRequest{
			ExcludeReqHeader: nil,
			MaxReqBody:       0,
			MaxRespBody:      0,
			Logger: func(ctx context.Context, format string, args ...interface{}) {
				log.Printf(format, args...)
			},
		}),
	))

	ctx := context.Background()
	err := f.Post(ctx, "api/user").
		JSON(map[string]interface{}{
			"name": "ming.liu",
			"age":  18,
		}).BindJSON(&res)
	if err != nil {
		t.Errorf("TestFetchPostJSON failed. err:%v", err)
		return
	}
	t.Logf("TestFetchPostJSON res:%+v", res)

	// output:
	/*
		2020/06/30 16:09:59 [Fetch] POST /api/user HTTP/1.1
		Host: 127.0.0.1:58717
		User-Agent: Go-http-client/1.1
		Transfer-Encoding: chunked
		Content-Type: application/json
		Accept-Encoding: gzip

		1c
		{"age":18,"name":"ming.liu"}
		0

		2020/06/30 16:09:59 [Fetch] HTTP/1.1 200 OK
		Content-Length: 22
		Content-Type: application/json
		Date: Tue, 30 Jun 2020 08:09:59 GMT
		X-Request-Id: trace-id-1593504599030600000

		{"code":0,"msg":"ok"}
		2020/06/30 16:09:59 [Fetch] method: POST, url: http://127.0.0.1:60661/api/user, header: map[Content-Type:[application/json]], body: {"age":18,"name":"ming.liu"}, latency: 995.441µs, status: 200, resp: {"code":0,"msg":"ok"}
		--- PASS: TestFetchPostJSON (0.00s)
		    fetch_test.go:283: TestFetchPostJSON res:{Data:<nil> Code:0 Msg:ok}
	*/
}
发送 application/xml 数据

Content-Type: "application/xml"

xml 数据发送和 json 数据支持格式一样

f := f.Post(ctx, "api/user")

xmlStr := `
<note>
    <to>George</to>
    <from>John</from>
    <heading>Reminder</heading>
    <body>Don't forget the meeting!</body>
</note>
`

f.XML(xmlStr)

示例:github.com/beanscc/fetch/fetch_test.go:TestFetchPostXML

func TestFetchPostXML(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("content-type", body.MIMEXML)
		w.Header().Add("x-request-id", fmt.Sprintf("trace-id-%d", time.Now().UnixNano()))
		out := newTestBaseResp(nil)
		fmt.Fprintln(w, out.xml())
	}))

	type User struct {
		XMLName xml.Name `xml:"user"`
		ID      string   `xml:"id,attr"`
		Name    string   `xml:"name"`
		Age     int      `xml:"age"`
		Height  float32  `xml:"height"`
	}

	ctx := context.Background()
	var res testBaseResp
	f := fetch.New(ts.URL, fetch.Debug(true))
	err := f.Post(ctx, "api/user").
		XML(&User{
			ID:     "6135200011057538",
			Name:   "si.li",
			Age:    20,
			Height: 175,
		}).BindXML(&res)
	if err != nil {
		t.Errorf("TestFetchPostXML failed. err:%v", err)
		return
	}
	t.Logf("TestFetchPostXML res:%+v", res)

	// output:
	/*
		2020/06/30 16:09:05 [Fetch] POST /api/user HTTP/1.1
		Host: 127.0.0.1:58708
		User-Agent: Go-http-client/1.1
		Transfer-Encoding: chunked
		Content-Type: application/xml
		Accept-Encoding: gzip

		56
		<user id="6135200011057538"><name>si.li</name><age>20</age><height>175</height></user>
		0

		2020/06/30 16:09:05 [Fetch] HTTP/1.1 200 OK
		Content-Length: 57
		Content-Type: application/xml
		Date: Tue, 30 Jun 2020 08:09:05 GMT
		X-Request-Id: trace-id-1593504545433384000

		<testBaseResp><code>0</code><msg>ok</msg></testBaseResp>
		--- PASS: TestFetchPostXML (0.00s)
		    fetch_test.go:340: TestFetchPostXML res:{Data:<nil> Code:0 Msg:ok}
	*/
}
发送 application/x-www-form-urlencoded 表单数据

Content-Type: "application/x-www-form-urlencoded"

f := f.Post(ctx, "user")

f.Form(map[string]interface{}{
	"name": "alice",
	"age":  12,
})

示例:github.com/beanscc/fetch/fetch_test.go:TestFetchPostForm

func TestFetchPostForm(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("content-type", body.MIMEJSON)
		w.Header().Add("x-request-id", fmt.Sprintf("trace-id-%d", time.Now().UnixNano()))
		out := newTestBaseResp(nil)
		fmt.Fprintln(w, out.json())
	}))

	ctx := context.Background()
	f := fetch.New(ts.URL, fetch.Debug(true))
	resBody, err := f.Post(ctx, "api/user").
		Form(map[string]interface{}{
			"name": "wang.wu",
			"age":  25,
		}).Text()
	if err != nil {
		t.Errorf("TestFetchPostForm failed. err:%v", err)
		return
	}
	t.Logf("TestFetchPostForm resp body:%s", resBody)

	// output:
	/*
		2020/06/30 16:08:06 [Fetch] POST /api/user HTTP/1.1
		Host: 127.0.0.1:58696
		User-Agent: Go-http-client/1.1
		Transfer-Encoding: chunked
		Content-Type: application/x-www-form-urlencoded
		Accept-Encoding: gzip

		13
		age=25&name=wang.wu
		0

		2020/06/30 16:08:06 [Fetch] HTTP/1.1 200 OK
		Content-Length: 22
		Content-Type: application/json
		Date: Tue, 30 Jun 2020 08:08:06 GMT
		X-Request-Id: trace-id-1593504486872096000

		{"code":0,"msg":"ok"}
		--- PASS: TestFetchPostForm (0.00s)
		    fetch_test.go:386: TestFetchPostForm resp body:{"code":0,"msg":"ok"}
	*/
}
发送 multipart/form-data 表单数据

Content-Type: "multipart/form-data"

可上传文件

示例:github.com/beanscc/fetch/fetch_test.go:TestFetchPostMultipartForm

func TestFetchPostMultipartForm(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("content-type", body.MIMEJSON)
		w.Header().Add("x-request-id", fmt.Sprintf("trace-id-%d", time.Now().UnixNano()))
		out := newTestBaseResp(nil)
		fmt.Fprintln(w, out.json())
	}))

	ctx := context.Background()
	f := fetch.New(ts.URL, fetch.Debug(true))

	formData := map[string]interface{}{
		"name": "wang.wu",
		"age":  25,
	}

	file1 := "testdata/f1.txt"
	file1Content, err := ioutil.ReadFile(file1)
	if err != nil {
		t.Fatalf("readFile 1 failed. err=%v", err)
	}

	file2 := "testdata/f2.txt"
	file2Content, err := ioutil.ReadFile(file2)
	if err != nil {
		t.Fatalf("readFile 2 failed. err=%v", err)
	}

	formFile := []body.File{
		{
			Field:    "file-1",
			Filename: file1, // note: 若未指定文件的 content-type,则表单发送时,根据文件内容识别此文件类型,此文件的 Content-Type: text/plain; charset=utf-8
			Content:  file1Content,
		},
		{
			Field:       "file-2",
			Filename:    file2,
			ContentType: "application/octet-stream", // note: 若指定文件的 content-type,则表单发送时,此文件的Content-Type: application/octet-stream
			Content:     file2Content,
		},
	}
	resBody, err := f.Post(ctx, "api/user").
		MultipartForm(formData, formFile...).Bytes()
	if err != nil {
		t.Errorf("TestFetchPostMultipartForm failed. err:%v", err)
		return
	}
	t.Logf("TestFetchPostMultipartForm resp body:%s", resBody)

	// output:
	/*
		2020/06/30 16:18:38 [Fetch] POST /api/user HTTP/1.1
		Host: 127.0.0.1:58880
		User-Agent: Go-http-client/1.1
		Transfer-Encoding: chunked
		Content-Type: multipart/form-data; boundary=3a27c156fa0406ed5b547dc7024c0fda21a5aa40536408dd40f95c5d0552
		Accept-Encoding: gzip

		2f5
		--3a27c156fa0406ed5b547dc7024c0fda21a5aa40536408dd40f95c5d0552
		Content-Disposition: form-data; name="name"

		wang.wu
		--3a27c156fa0406ed5b547dc7024c0fda21a5aa40536408dd40f95c5d0552
		Content-Disposition: form-data; name="age"

		25
		--3a27c156fa0406ed5b547dc7024c0fda21a5aa40536408dd40f95c5d0552
		Content-Disposition: form-data; name="file-1"; filename="testdata/f1.txt"
		Content-Type: text/plain; charset=utf-8

		this is test file.
		this is test file line 2;
		--3a27c156fa0406ed5b547dc7024c0fda21a5aa40536408dd40f95c5d0552
		Content-Disposition: form-data; name="file-2"; filename="testdata/f2.txt"
		Content-Type: application/octet-stream

		this is test file2.

		this is test file line 3;
		--3a27c156fa0406ed5b547dc7024c0fda21a5aa40536408dd40f95c5d0552--

		0

		2020/06/30 16:18:38 [Fetch] HTTP/1.1 200 OK
		Content-Length: 22
		Content-Type: application/json
		Date: Tue, 30 Jun 2020 08:18:38 GMT
		X-Request-Id: trace-id-1593505118084005000

		{"code":0,"msg":"ok"}
		--- PASS: TestFetchPostMultipartForm (0.00s)
		    fetch_test.go:365: TestFetchPostMultipartForm resp body:{"code":0,"msg":"ok"}
	*/
}
Resp 响应解析
f := f.Post(ctx, "api/user")

// 支持 string 类型 json 字符串
f.JSON(`{"name": "alice", "age": 12}`)

// 获取 *http.Response, 及响应消息体
res, resBytes, err := f.Resp()

// 获取 http 响应 body 消息体的 []byte
resBytes, err := f.Bytes()

// 获取 http 响应 body 消息体的 string
resStr, err := f.Text()

// ==== 对响应body消息体进行结构化解析 ====

// 以 json 格式解析
err := f.BindJSON(&resJson)

// 以 xml 格式解析
err := f.BindXML(&resXml)

fetch.New() 创建的 Fetch 对象已注册了默认的 jsonxml 格式解析函数

如何自定义解析器
// 先注册解析器
// 方式1: New() 时,注册 Bind option
f := fetch.New("", fetch.Bind(map[string]binding.Binding{"custom-bind-type", customBindFn}))

// 方式2: 通过 WithOptions() 注册 Bind
f.WithOptions(fetch.Bind(map[string]binding.Binding{"custom-bind-type", customBindFn}))

// 解析使用
err := f.Bind("custom-bind-type", customBindFn)
Timeout 超时控制
// 方式1. 通过 Timeout 全局超时
f := fetch.New("", Timeout(10 * time.Second))
// 或
f = f.WithOptions(Timeout(10 * time.Second))

// 方式2. 通过 ctx 单次请求超时设置
ctx, cancel := context.WithTimeout(context.Background, 10 * time.Second)
defer cancel()
f = f.Get(ctx, "api/user")
Interceptor 拦截器

拦截器可以做什么?

  • 记录每次请求及响应数据的日志信息,可参见 fetch.LogInterceptor()
  • 对请求进行打点上报请求质量状况
  • 在请求前对参数进行签名
  • 请求前对参数/body消息体进行加密,响应后对消息体进行加解密
  • 自定义请求进行重试,自定义何种情况/多少时间间隔/重试多少次

请注意拦截器执行的顺序流程,合理安排多个拦截器之间的顺序关系

拦截点:

  • 发送 http 请求前,对请求进行拦截,可对请求数据进行预处理
  • 请求发送后,对响应进行拦截,可对响应进行预处理

多个拦截器的执行顺序是什么? 先来看一下关于拦截器的定义

// Handler http req handle
type Handler func(ctx context.Context, req *http.Request) (*http.Response, []byte, error)

// Interceptor 请求拦截器
// 多个 interceptor one,two,three 则执行顺序是 one,two,three 的 handler 调用前的执行流,然后是 handler, 接着是 three,two,one 中 handler 调用之后的执行流
type Interceptor func(ctx context.Context, req *http.Request, handler Handler) (*http.Response, []byte, error)

// chainInterceptor 将多个 Interceptor 合并为一个
func chainInterceptor(interceptors ...Interceptor) Interceptor {
	n := len(interceptors)
	if n > 1 {
		lastI := n - 1
		return func(ctx context.Context, req *http.Request, handler Handler) (*http.Response, []byte, error) {
			var (
				chainHandler Handler
				curI         int
			)

			chainHandler = func(currentCtx context.Context, currentReq *http.Request) (*http.Response, []byte, error) {
				if curI == lastI {
					return handler(currentCtx, currentReq)
				}
				curI++
				resp, body, err := interceptors[curI](currentCtx, currentReq, chainHandler)
				curI--
				return resp, body, err
			}

			return interceptors[0](ctx, req, chainHandler)
		}
	}

	if n == 1 {
		return interceptors[0]
	}

	// n == 0; Dummy interceptor maintained for backward compatibility to avoid returning nil.
	return func(ctx context.Context, req *http.Request, handler Handler) (*http.Response, []byte, error) {
		return handler(ctx, req)
	}
}

多个拦截器在执行时,首先会合并为一个拦截器(通过函数 chainInterceptor() 可以将多个拦截器合并成一个

合并后的拦截器的执行流是怎样的呢?

首先,拦截器方法中 handler 回调函数,主要是执行 Client.Do(),在拦截器中,handler 之前的流程被认为是拦截器的第一个拦截点,handler 之后的执行流,被认为是第二个拦截点

那么按照上面合并后的执行流,若有多个 interceptor one,two,three 则执行顺序是 one,two,three 的 handler 调用前的执行流,然后是 handler, 紧接着是 three,two,one 中 handler 调用之后的执行流

注意:有 2 个点需要注意

  • 在 handler 之前对请求数据进行处理时,若需要读取 req.Body , 请使用 req.GetBody() 或 util.DrainBody() 方法进行读取,否则,前面的拦截器将 req.Body 数据读取后,后面的拦截器就无法读取请求body体数据了,发送 http 请求时,也将丢失 body 消息体
  • 在 handler 之后对请求响应数据进行处理时,若需要读取 resp.Body,可使用 util.DrainBody() 函数拷贝并重置 resp.Body,否则和读取请求body 消息体一样,后面将无法从响应body中读取body消息体

如何注册拦截器?

// 通过 Interceptors 设置拦截器
f := fetch.New(ts.URL,
		fetch.Debug(true),
		fetch.Interceptors(
			// fetch.LogInterceptor 会输出以下日志内容
			// 2020/06/30 16:12:22 [Fetch] method: GET, url: http://127.0.0.1:58785/api/user?id=10&name=ming, header: map[X-Request-Id:[trace-id-1593504742037996000]], body: , latency: 1.088405ms, status: 200, resp: {"data":{"name":"ming.liu","age":20,"address":"beijing wangfujing street","mobile":"+86-13800000000"},"code":0,"msg":"ok"}, err: <nil>, extra k1:v1
			fetch.LogInterceptor(&fetch.LogInterceptorRequest{
				ExcludeReqHeader: nil,
				MaxReqBody:       0,
				MaxRespBody:      0,
				Logger: func(ctx context.Context, format string, args ...interface{}) {
					v1, _ := ctx.Value("k1").(string)
					log.Printf(format+", extra k1:%v", append(args, v1)...)
				},
			}),
		))

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	DefaultLogInterceptor = LogInterceptor(&LogInterceptorRequest{})
)

Functions

This section is empty.

Types

type Fetch

type Fetch struct {
	// contains filtered or unexported fields
}

Fetch ...

func Delete

func Delete(ctx context.Context, url string, params ...interface{}) *Fetch

func Get

func Get(ctx context.Context, url string, params ...interface{}) *Fetch
func Head(ctx context.Context, url string, params ...interface{}) *Fetch

func New

func New(baseURL string, options ...Option) *Fetch

New return new Fetch

func Post

func Post(ctx context.Context, url string, params ...interface{}) *Fetch

func Put

func Put(ctx context.Context, url string, params ...interface{}) *Fetch

func (*Fetch) AddCookie

func (f *Fetch) AddCookie(cs ...*http.Cookie) *Fetch

func (*Fetch) AddHeader

func (f *Fetch) AddHeader(args ...interface{}) *Fetch

AddHeader 添加 http header args 支持 key-val 对,或 map[string]interface{},或者 key-val 对和map[string]interface{}交替组合

func (*Fetch) Bind

func (f *Fetch) Bind(bind binding.Binding, v interface{}) error

Bind 按已注册 bind 类型,解析 http 响应

func (*Fetch) BindJSON

func (f *Fetch) BindJSON(v interface{}) error

BindJSON bind http.Body with json

func (*Fetch) BindXML

func (f *Fetch) BindXML(v interface{}) error

BindXML bind http.Body with xml

func (*Fetch) Body

func (f *Fetch) Body(b body.Body) *Fetch

Body 设置请求的 body 消息体

func (*Fetch) Bytes

func (f *Fetch) Bytes() ([]byte, error)

Bytes 返回http响应body消息体

func (*Fetch) Context

func (f *Fetch) Context() context.Context

Context return context

func (*Fetch) Delete

func (f *Fetch) Delete(ctx context.Context, path string, params ...interface{}) *Fetch

Delete del 请求

func (*Fetch) Form

func (f *Fetch) Form(data interface{}) *Fetch

Form 发送 x-www-form-urlencoded 格式消息 data 支持 map[string]interface{} 或 url.Values

func (*Fetch) Get

func (f *Fetch) Get(ctx context.Context, path string, params ...interface{}) *Fetch

Get get 请求

func (*Fetch) Head

func (f *Fetch) Head(ctx context.Context, path string, params ...interface{}) *Fetch

Head 请求

func (*Fetch) JSON

func (f *Fetch) JSON(data interface{}) *Fetch

JSON 发送 application/json 格式消息 p 支持 string/[]byte/其他类型按 json.Marshal 编码

func (*Fetch) Method

func (f *Fetch) Method(ctx context.Context, method string, path string, params ...interface{}) *Fetch

func (*Fetch) MultipartForm

func (f *Fetch) MultipartForm(data interface{}, fs ...body.File) *Fetch

MultipartForm 发送 multipart/form-data 格式消息 data 支持 map[string]interface{} 或 url.Values

func (*Fetch) Post

func (f *Fetch) Post(ctx context.Context, path string, params ...interface{}) *Fetch

Post post 请求

func (*Fetch) Put

func (f *Fetch) Put(ctx context.Context, path string, params ...interface{}) *Fetch

Put put 请求

func (*Fetch) Query

func (f *Fetch) Query(args ...interface{}) *Fetch

Query 设置查询参数 args 支持 key-val 对,或 map[string]interface{},或者 key-val 对和map[string]interface{}交替组合 Query("k1", 1, "k2", 2, map[string]interface{}{"k3": "v3"})

func (*Fetch) Resp

func (f *Fetch) Resp() (*http.Response, []byte, error)

Resp return http.Response, resp body, err

func (*Fetch) SetBasicAuth

func (f *Fetch) SetBasicAuth(username, password string) *Fetch

func (*Fetch) SetHeader

func (f *Fetch) SetHeader(args ...interface{}) *Fetch

SetHeader 设置 http header args 支持 key-val 对,或 map[string]interface{},或者 key-val 对和map[string]interface{}交替组合

func (*Fetch) Text

func (f *Fetch) Text() (string, error)

Text 返回http响应body消息体

func (*Fetch) WithOptions

func (f *Fetch) WithOptions(options ...Option) *Fetch

WithOptions 返回一个设置了新 option 的 *Fetch 对象

func (*Fetch) XML

func (f *Fetch) XML(data interface{}) *Fetch

XML 发送 application/xml 格式消息 p 支持 string/[]byte/其他类型按 xml.Marshal 编码

type Handler

type Handler func(ctx context.Context, req *http.Request) (*http.Response, []byte, error)

Handler http req handle

type Interceptor

type Interceptor func(ctx context.Context, req *http.Request, handler Handler) (*http.Response, []byte, error)

Interceptor 请求拦截器 多个 interceptor one,two,three 则执行顺序是 one,two,three 的 handler 调用前的执行流,然后是 handler, 接着是 three,two,one 中 handler 调用之后的执行流

func LogInterceptor

func LogInterceptor(param *LogInterceptorRequest) Interceptor

type LogInterceptorRequest

type LogInterceptorRequest struct {
	ExcludeReqHeader map[string]bool                                               // 日志不记录的请求头
	MaxReqBody       int                                                           // 日志记录请求消息体的最大字节数
	MaxRespBody      int                                                           // 日志记录响应消息体的最大字节数
	Logger           func(ctx context.Context, format string, args ...interface{}) // 日志记录的方法
}

type Option

type Option interface {
	Apply(*Fetch)
}

Option 用于设置 Fetch 属性的接口

func Bind

func Bind(binds map[string]binding.Binding) Option

Bind 设置自定义响应解析器

func Client

func Client(c *http.Client) Option

设置 client

func Debug

func Debug(debug bool) Option

Debug 设置 debug

func Interceptors

func Interceptors(interceptors ...Interceptor) Option

设置 Interceptor

func Timeout

func Timeout(t time.Duration) Option

Timeout 设置超时时间

type Options

type Options struct {
	Debug        bool
	Timeout      time.Duration
	Bind         map[string]binding.Binding
	Client       *http.Client
	Interceptors []Interceptor
}

Options 用于设置 Fetch 属性

func (*Options) Apply

func (o *Options) Apply(f *Fetch)

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL