项目作者: beanscc

项目描述 :
http request
高级语言: Go
项目地址: git://github.com/beanscc/fetch.git
创建时间: 2019-01-04T07:58:37Z
项目社区:https://github.com/beanscc/fetch

开源协议:MIT License

下载


fetch

http client 网络请求封装

Overview

涵盖功能:

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

Contents

Installation

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

Quick Start

  1. // github.com/beanscc/fetch/examples/basic/main.go
  2. package main
  3. import (
  4. "context"
  5. "encoding/json"
  6. "log"
  7. "net/http"
  8. "net/http/httptest"
  9. "github.com/beanscc/fetch"
  10. "github.com/beanscc/fetch/body"
  11. )
  12. func main() {
  13. type Resp struct {
  14. Name string `json:"name"`
  15. Age uint8 `json:"age"`
  16. Addr string `json:"address"`
  17. Mobile string `json:"mobile"`
  18. }
  19. ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  20. out := baseResp{
  21. Code: 0,
  22. Msg: "ok",
  23. Data: &Resp{
  24. Name: "ming.liu",
  25. Age: 20,
  26. Addr: "beijing wangfujing street",
  27. Mobile: "+86-13800000000",
  28. },
  29. }
  30. res, _ := json.Marshal(out)
  31. w.Header().Set("content-type", body.MIMEJSON)
  32. w.WriteHeader(http.StatusOK)
  33. _, _ = w.Write(res)
  34. }))
  35. // var data Resp
  36. // res := newBaseResp(&data)
  37. // err := fetch.Get(context.Background(), ts.URL+"/api/user").
  38. // Query("id", 10).
  39. // BindJSON(&res)
  40. // OR
  41. f := fetch.New(ts.URL, &fetch.Options{
  42. Debug: true,
  43. Interceptors: []fetch.Interceptor{
  44. // fetch.LogInterceptor 会输出请求和响应日志
  45. fetch.LogInterceptor(&fetch.LogInterceptorRequest{
  46. MaxReqBody: 5,
  47. // MaxRespBody: 10,
  48. Logger: func(ctx context.Context, format string, args ...interface{}) {
  49. v1, _ := ctx.Value("k1").(string)
  50. allArgs := []interface{}{v1}
  51. allArgs = append(allArgs, args...)
  52. log.Printf("extra k1:%v, "+format, allArgs...)
  53. },
  54. }),
  55. },
  56. })
  57. ctx := context.WithValue(context.Background(), "k1", "v1")
  58. var data Resp
  59. res := newBaseResp(&data)
  60. err := f.Post(ctx, "api/user").
  61. AddHeader("hk_1", "hk_1_val").
  62. AddHeader(map[string]interface{}{
  63. "hk_2": 24,
  64. "hk_3": "hk_3_val",
  65. }).
  66. AddHeader("hk_4", 4, map[string]interface{}{"hk_5": 66.66}, "hk_6", "hk_6_val").
  67. SetHeader("hk_1", 111).
  68. Query("id", 10).
  69. JSON(`{"age": 18}`).
  70. BindJSON(&res)
  71. if err != nil {
  72. log.Printf("fetch.Get() failed. err:%v", err)
  73. return
  74. }
  75. log.Printf("fetch.Get() data:%+v", res.Data) // output: fetch.Get() data:&{Name:ming.liu Age:20 Addr:beijing wangfujing street Mobile:+86-13800000000}
  76. // output:
  77. /*
  78. 2020/07/01 16:41:38 [Fetch] GET /api/user?id=10 HTTP/1.1
  79. Host: 127.0.0.1:50305
  80. User-Agent: Go-http-client/1.1
  81. Hk_1: 111
  82. Hk_2: 24
  83. Hk_3: hk_3_val
  84. Hk_4: 4
  85. Hk_5: 66.66
  86. Hk_6: hk_6_val
  87. Accept-Encoding: gzip
  88. b
  89. {"age": 18}
  90. 0
  91. 2020/07/01 16:41:38 [Fetch] HTTP/1.1 200 OK
  92. Content-Length: 122
  93. Content-Type: application/json
  94. Date: Wed, 01 Jul 2020 08:41:38 GMT
  95. {"data":{"name":"ming.liu","age":20,"address":"beijing wangfujing street","mobile":"+86-13800000000"},"code":0,"msg":"ok"}
  96. 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>
  97. 2020/07/01 16:41:38 fetch.Get() data:&{Name:ming.liu Age:20 Addr:beijing wangfujing street Mobile:+86-13800000000}
  98. */
  99. }
  100. type baseResp struct {
  101. Data interface{} `json:"data,empty"`
  102. Code int `json:"code"`
  103. Msg string `json:"msg"`
  104. }
  105. func newBaseResp(data interface{}) *baseResp {
  106. return &baseResp{
  107. Data: data,
  108. Code: 0,
  109. Msg: "ok",
  110. }
  111. }

API Examples

创建 Fetch

  1. // 不指定 Fetch 客户端请求的基础域名,需要在 Get/Post... 等方法时,使用绝对地址
  2. f := fetch.New("")
  3. // 指定基础域名地址, 后面 Get/Post ... 等方法调用时,可使用相对地址,也可以使用绝对地址
  4. f2 := fetch.New("http://api.domain.com")
  5. // 指定基础域名,同时开启 debug 模式(debug 模式,将使用标准包 "log" 以文本格式,输出请求和响应的详细日志)
  6. f3 := fetch.New("http://api.domain.com", fetch.Debug(true))
  7. f4 := fetch.New("", &fetch.Options{
  8. Debug: true,
  9. Timeout: 10 * time.Second,
  10. })

Options 的使用

Debug

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

  1. f := fetch.New(ts.URL, fetch.Debug(true)).
  2. Get(context.Background(), "api/user").
  3. Query("id", 10).
  4. Bytes()
  5. // output
  6. /*
  7. 2020/06/30 01:15:55 [Fetch] GET /api/user?id=10 HTTP/1.1
  8. Host: 127.0.0.1:49893
  9. User-Agent: Go-http-client/1.1
  10. Accept-Encoding: gzip
  11. 2020/06/30 01:15:55 [Fetch] HTTP/1.1 200 OK
  12. Content-Length: 119
  13. Content-Type: application/json
  14. Date: Mon, 29 Jun 2020 17:15:55 GMT
  15. {"data":{"addr":"beijing wangfujing street","age":20,"mobile":"+86-13800000000","name":"ming.liu"},"code":0,"msg":"ok"}
  16. */
Timeout
  1. // Timeout 设置 Fetch 全局超时
  2. f := fetch.New("", fetch.Timeout(10*time.Second))
  3. // 或 设置某次请求的超时时间
  4. f = f.WithOptions(fetch.Timeout(3 * time.Second))

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

Method 设置

  1. // 使用默认 Fetch
  2. f := fetch.Get(context.Background(), "api/user")
  3. // 自定义 Fetch 的基础域名,同时通过 timeout option 设置每次请求的超时时间
  4. f1 := fetch.New("http://api.domain.com/", fetch.Timeout(10 *time.Second))
  5. f1tmp := f1.Get(context.Background, "api/user").
  6. Query("id",10)
  7. ...
  8. f2tmp := f1.Post(context.Background, "api/user").
  9. JSON(map[string]interface{}{"id": 10, "name": "ming.liu"})
  10. ...
  11. f3tmp := f1.Method("Get", "api/user")
  12. ...

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

  1. // 错误使用方式
  2. f.Get(ctx, "city") // 需要接收 Get() 返回的新 *Fetch 对象,下面的操作才不会出错
  3. b, err := f.Query("id", "1").Text() // err != nil, err="fetch: empty method"
  4. // 正确的方式:将下面的操作组成一个链式操作
  5. b, err := f.Get(ctx, "city").Query("id", "1").Text()
  6. // 或
  7. f = f.Get(ctx, "city")
  8. b, err := f.Query("id", 1).Text()

path 动态参数设置

  1. f := fetch.Get(ctx, "api/user/:uid/address/:address_id", 1, 20)
  2. // 就是请求 api/user/1/address/20

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

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

Query 设置

  1. f = f.Get(ctx, "api/user")
  2. // query 参数以 key/val 对形式设置(必须成对)
  3. f = f.Query("id", 1).Query("age", 12).Query("name", "ming.liu")
  4. // 或通过 map[string]interface{} 一次设置多个key/val对
  5. f1 := f.Query(map[string]interface{}{
  6. "id": 1,
  7. "age": 12,
  8. })
  9. // 或者 key/val 对和 map[string]interface{} 交替形式设置
  10. f2 := f.Query("id", 1, map[string]interface{}{
  11. "age": 12,
  12. "name": "ming.liu",
  13. }, "height", 175)

Header 设置

  1. f = f.Get(ctx, "city")
  2. // AddHeader 传参数方式类似 Query 传参
  3. f.AddHeader("hk_1", "hk_1_val").
  4. AddHeader(map[string]interface{}{
  5. "hk_2": 24,
  6. "hk_3": "hk_3_val",
  7. }).
  8. AddHeader("hk_4", 4, map[string]interface{}{"hk_5": 66.66}, "hk_6", "hk_6_val")
  9. // SetHeader 和 AddHeader 一样
  10. f.SetHeader("app-time", time.Now().UnixNano()) // 将覆盖上面 "app-time" 的值

Body 设置

  1. // import "github.com/beanscc/fetch/body"
  2. // Body 构造请求的body
  3. type Body interface {
  4. // Body 构造http请求body
  5. Body() (io.Reader, error)
  6. // ContentType 返回 body 体结构相应的 Header content-type 类型
  7. ContentType() string
  8. }
  9. // Body 接口实现检查
  10. var (
  11. _ Body = &JSON{}
  12. _ Body = &XML{}
  13. _ Body = &Form{}
  14. _ Body = &MultipartForm{}
  15. )

发送 application/json 数据

Content-Type: “application/json”

  1. f := f.Post(ctx, "api/user")
  2. // 支持 string 类型 json 字符串
  3. f.JSON(`{"name": "alice", "age": 12}`)
  4. // 支持 []byte 类型 json
  5. f.JSON([]byte(`{"name": "alice", "age": 12}`))
  6. // 非 string / []byte 类型,将都调用 json.Marshal 进行序列化
  7. f.JSON(map[string]interface{}{
  8. "name": "alice",
  9. "age": 12,
  10. })
  11. type User struct {
  12. Name string `json:"name"`
  13. Age int `json:"age"`
  14. }
  15. user := User{Name: "alice", Age: 12}
  16. f.JSON(user)

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

  1. func TestFetchPostJSON(t *testing.T) {
  2. ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  3. w.Header().Set("content-type", body.MIMEJSON)
  4. w.Header().Add("x-request-id", fmt.Sprintf("trace-id-%d", time.Now().UnixNano()))
  5. out := newTestBaseResp(nil)
  6. fmt.Fprintln(w, out.json())
  7. }))
  8. var res testBaseResp
  9. f := fetch.New(ts.URL, fetch.Debug(true), fetch.Interceptors(
  10. // fetch.LogInterceptor 会输出以下日志内容
  11. fetch.LogInterceptor(&fetch.LogInterceptorRequest{
  12. ExcludeReqHeader: nil,
  13. MaxReqBody: 0,
  14. MaxRespBody: 0,
  15. Logger: func(ctx context.Context, format string, args ...interface{}) {
  16. log.Printf(format, args...)
  17. },
  18. }),
  19. ))
  20. ctx := context.Background()
  21. err := f.Post(ctx, "api/user").
  22. JSON(map[string]interface{}{
  23. "name": "ming.liu",
  24. "age": 18,
  25. }).BindJSON(&res)
  26. if err != nil {
  27. t.Errorf("TestFetchPostJSON failed. err:%v", err)
  28. return
  29. }
  30. t.Logf("TestFetchPostJSON res:%+v", res)
  31. // output:
  32. /*
  33. 2020/06/30 16:09:59 [Fetch] POST /api/user HTTP/1.1
  34. Host: 127.0.0.1:58717
  35. User-Agent: Go-http-client/1.1
  36. Transfer-Encoding: chunked
  37. Content-Type: application/json
  38. Accept-Encoding: gzip
  39. 1c
  40. {"age":18,"name":"ming.liu"}
  41. 0
  42. 2020/06/30 16:09:59 [Fetch] HTTP/1.1 200 OK
  43. Content-Length: 22
  44. Content-Type: application/json
  45. Date: Tue, 30 Jun 2020 08:09:59 GMT
  46. X-Request-Id: trace-id-1593504599030600000
  47. {"code":0,"msg":"ok"}
  48. 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"}
  49. --- PASS: TestFetchPostJSON (0.00s)
  50. fetch_test.go:283: TestFetchPostJSON res:{Data:<nil> Code:0 Msg:ok}
  51. */
  52. }

发送 application/xml 数据

Content-Type: “application/xml”

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

  1. f := f.Post(ctx, "api/user")
  2. xmlStr := `
  3. <note>
  4. <to>George</to>
  5. <from>John</from>
  6. <heading>Reminder</heading>
  7. <body>Don't forget the meeting!</body>
  8. </note>
  9. `
  10. f.XML(xmlStr)

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

  1. func TestFetchPostXML(t *testing.T) {
  2. ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  3. w.Header().Set("content-type", body.MIMEXML)
  4. w.Header().Add("x-request-id", fmt.Sprintf("trace-id-%d", time.Now().UnixNano()))
  5. out := newTestBaseResp(nil)
  6. fmt.Fprintln(w, out.xml())
  7. }))
  8. type User struct {
  9. XMLName xml.Name `xml:"user"`
  10. ID string `xml:"id,attr"`
  11. Name string `xml:"name"`
  12. Age int `xml:"age"`
  13. Height float32 `xml:"height"`
  14. }
  15. ctx := context.Background()
  16. var res testBaseResp
  17. f := fetch.New(ts.URL, fetch.Debug(true))
  18. err := f.Post(ctx, "api/user").
  19. XML(&User{
  20. ID: "6135200011057538",
  21. Name: "si.li",
  22. Age: 20,
  23. Height: 175,
  24. }).BindXML(&res)
  25. if err != nil {
  26. t.Errorf("TestFetchPostXML failed. err:%v", err)
  27. return
  28. }
  29. t.Logf("TestFetchPostXML res:%+v", res)
  30. // output:
  31. /*
  32. 2020/06/30 16:09:05 [Fetch] POST /api/user HTTP/1.1
  33. Host: 127.0.0.1:58708
  34. User-Agent: Go-http-client/1.1
  35. Transfer-Encoding: chunked
  36. Content-Type: application/xml
  37. Accept-Encoding: gzip
  38. 56
  39. <user id="6135200011057538"><name>si.li</name><age>20</age><height>175</height></user>
  40. 0
  41. 2020/06/30 16:09:05 [Fetch] HTTP/1.1 200 OK
  42. Content-Length: 57
  43. Content-Type: application/xml
  44. Date: Tue, 30 Jun 2020 08:09:05 GMT
  45. X-Request-Id: trace-id-1593504545433384000
  46. <testBaseResp><code>0</code><msg>ok</msg></testBaseResp>
  47. --- PASS: TestFetchPostXML (0.00s)
  48. fetch_test.go:340: TestFetchPostXML res:{Data:<nil> Code:0 Msg:ok}
  49. */
  50. }

发送 application/x-www-form-urlencoded 表单数据

Content-Type: “application/x-www-form-urlencoded”

  1. f := f.Post(ctx, "user")
  2. f.Form(map[string]interface{}{
  3. "name": "alice",
  4. "age": 12,
  5. })

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

  1. func TestFetchPostForm(t *testing.T) {
  2. ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  3. w.Header().Set("content-type", body.MIMEJSON)
  4. w.Header().Add("x-request-id", fmt.Sprintf("trace-id-%d", time.Now().UnixNano()))
  5. out := newTestBaseResp(nil)
  6. fmt.Fprintln(w, out.json())
  7. }))
  8. ctx := context.Background()
  9. f := fetch.New(ts.URL, fetch.Debug(true))
  10. resBody, err := f.Post(ctx, "api/user").
  11. Form(map[string]interface{}{
  12. "name": "wang.wu",
  13. "age": 25,
  14. }).Text()
  15. if err != nil {
  16. t.Errorf("TestFetchPostForm failed. err:%v", err)
  17. return
  18. }
  19. t.Logf("TestFetchPostForm resp body:%s", resBody)
  20. // output:
  21. /*
  22. 2020/06/30 16:08:06 [Fetch] POST /api/user HTTP/1.1
  23. Host: 127.0.0.1:58696
  24. User-Agent: Go-http-client/1.1
  25. Transfer-Encoding: chunked
  26. Content-Type: application/x-www-form-urlencoded
  27. Accept-Encoding: gzip
  28. 13
  29. age=25&name=wang.wu
  30. 0
  31. 2020/06/30 16:08:06 [Fetch] HTTP/1.1 200 OK
  32. Content-Length: 22
  33. Content-Type: application/json
  34. Date: Tue, 30 Jun 2020 08:08:06 GMT
  35. X-Request-Id: trace-id-1593504486872096000
  36. {"code":0,"msg":"ok"}
  37. --- PASS: TestFetchPostForm (0.00s)
  38. fetch_test.go:386: TestFetchPostForm resp body:{"code":0,"msg":"ok"}
  39. */
  40. }

发送 multipart/form-data 表单数据

Content-Type: “multipart/form-data”

可上传文件

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

  1. func TestFetchPostMultipartForm(t *testing.T) {
  2. ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  3. w.Header().Set("content-type", body.MIMEJSON)
  4. w.Header().Add("x-request-id", fmt.Sprintf("trace-id-%d", time.Now().UnixNano()))
  5. out := newTestBaseResp(nil)
  6. fmt.Fprintln(w, out.json())
  7. }))
  8. ctx := context.Background()
  9. f := fetch.New(ts.URL, fetch.Debug(true))
  10. formData := map[string]interface{}{
  11. "name": "wang.wu",
  12. "age": 25,
  13. }
  14. file1 := "testdata/f1.txt"
  15. file1Content, err := ioutil.ReadFile(file1)
  16. if err != nil {
  17. t.Fatalf("readFile 1 failed. err=%v", err)
  18. }
  19. file2 := "testdata/f2.txt"
  20. file2Content, err := ioutil.ReadFile(file2)
  21. if err != nil {
  22. t.Fatalf("readFile 2 failed. err=%v", err)
  23. }
  24. formFile := []body.File{
  25. {
  26. Field: "file-1",
  27. Filename: file1, // note: 若未指定文件的 content-type,则表单发送时,根据文件内容识别此文件类型,此文件的 Content-Type: text/plain; charset=utf-8
  28. Content: file1Content,
  29. },
  30. {
  31. Field: "file-2",
  32. Filename: file2,
  33. ContentType: "application/octet-stream", // note: 若指定文件的 content-type,则表单发送时,此文件的Content-Type: application/octet-stream
  34. Content: file2Content,
  35. },
  36. }
  37. resBody, err := f.Post(ctx, "api/user").
  38. MultipartForm(formData, formFile...).Bytes()
  39. if err != nil {
  40. t.Errorf("TestFetchPostMultipartForm failed. err:%v", err)
  41. return
  42. }
  43. t.Logf("TestFetchPostMultipartForm resp body:%s", resBody)
  44. // output:
  45. /*
  46. 2020/06/30 16:18:38 [Fetch] POST /api/user HTTP/1.1
  47. Host: 127.0.0.1:58880
  48. User-Agent: Go-http-client/1.1
  49. Transfer-Encoding: chunked
  50. Content-Type: multipart/form-data; boundary=3a27c156fa0406ed5b547dc7024c0fda21a5aa40536408dd40f95c5d0552
  51. Accept-Encoding: gzip
  52. 2f5
  53. --3a27c156fa0406ed5b547dc7024c0fda21a5aa40536408dd40f95c5d0552
  54. Content-Disposition: form-data; name="name"
  55. wang.wu
  56. --3a27c156fa0406ed5b547dc7024c0fda21a5aa40536408dd40f95c5d0552
  57. Content-Disposition: form-data; name="age"
  58. 25
  59. --3a27c156fa0406ed5b547dc7024c0fda21a5aa40536408dd40f95c5d0552
  60. Content-Disposition: form-data; name="file-1"; filename="testdata/f1.txt"
  61. Content-Type: text/plain; charset=utf-8
  62. this is test file.
  63. this is test file line 2;
  64. --3a27c156fa0406ed5b547dc7024c0fda21a5aa40536408dd40f95c5d0552
  65. Content-Disposition: form-data; name="file-2"; filename="testdata/f2.txt"
  66. Content-Type: application/octet-stream
  67. this is test file2.
  68. this is test file line 3;
  69. --3a27c156fa0406ed5b547dc7024c0fda21a5aa40536408dd40f95c5d0552--
  70. 0
  71. 2020/06/30 16:18:38 [Fetch] HTTP/1.1 200 OK
  72. Content-Length: 22
  73. Content-Type: application/json
  74. Date: Tue, 30 Jun 2020 08:18:38 GMT
  75. X-Request-Id: trace-id-1593505118084005000
  76. {"code":0,"msg":"ok"}
  77. --- PASS: TestFetchPostMultipartForm (0.00s)
  78. fetch_test.go:365: TestFetchPostMultipartForm resp body:{"code":0,"msg":"ok"}
  79. */
  80. }

Resp 响应解析

  1. f := f.Post(ctx, "api/user")
  2. // 支持 string 类型 json 字符串
  3. f.JSON(`{"name": "alice", "age": 12}`)
  4. // 获取 *http.Response, 及响应消息体
  5. res, resBytes, err := f.Resp()
  6. // 获取 http 响应 body 消息体的 []byte
  7. resBytes, err := f.Bytes()
  8. // 获取 http 响应 body 消息体的 string
  9. resStr, err := f.Text()
  10. // ==== 对响应body消息体进行结构化解析 ====
  11. // 以 json 格式解析
  12. err := f.BindJSON(&resJson)
  13. // 以 xml 格式解析
  14. err := f.BindXML(&resXml)

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

如何自定义解析器

  1. // 先注册解析器
  2. // 方式1: New() 时,注册 Bind option
  3. f := fetch.New("", fetch.Bind(map[string]binding.Binding{"custom-bind-type", customBindFn}))
  4. // 方式2: 通过 WithOptions() 注册 Bind
  5. f.WithOptions(fetch.Bind(map[string]binding.Binding{"custom-bind-type", customBindFn}))
  6. // 解析使用
  7. err := f.Bind("custom-bind-type", customBindFn)

Timeout 超时控制

  1. // 方式1. 通过 Timeout 全局超时
  2. f := fetch.New("", Timeout(10 * time.Second))
  3. // 或
  4. f = f.WithOptions(Timeout(10 * time.Second))
  5. // 方式2. 通过 ctx 单次请求超时设置
  6. ctx, cancel := context.WithTimeout(context.Background, 10 * time.Second)
  7. defer cancel()
  8. f = f.Get(ctx, "api/user")

Interceptor 拦截器

拦截器可以做什么?

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

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

拦截点:

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

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

  1. // Handler http req handle
  2. type Handler func(ctx context.Context, req *http.Request) (*http.Response, []byte, error)
  3. // Interceptor 请求拦截器
  4. // 多个 interceptor one,two,three 则执行顺序是 one,two,three 的 handler 调用前的执行流,然后是 handler, 接着是 three,two,one 中 handler 调用之后的执行流
  5. type Interceptor func(ctx context.Context, req *http.Request, handler Handler) (*http.Response, []byte, error)
  6. // chainInterceptor 将多个 Interceptor 合并为一个
  7. func chainInterceptor(interceptors ...Interceptor) Interceptor {
  8. n := len(interceptors)
  9. if n > 1 {
  10. lastI := n - 1
  11. return func(ctx context.Context, req *http.Request, handler Handler) (*http.Response, []byte, error) {
  12. var (
  13. chainHandler Handler
  14. curI int
  15. )
  16. chainHandler = func(currentCtx context.Context, currentReq *http.Request) (*http.Response, []byte, error) {
  17. if curI == lastI {
  18. return handler(currentCtx, currentReq)
  19. }
  20. curI++
  21. resp, body, err := interceptors[curI](currentCtx, currentReq, chainHandler)
  22. curI--
  23. return resp, body, err
  24. }
  25. return interceptors[0](ctx, req, chainHandler)
  26. }
  27. }
  28. if n == 1 {
  29. return interceptors[0]
  30. }
  31. // n == 0; Dummy interceptor maintained for backward compatibility to avoid returning nil.
  32. return func(ctx context.Context, req *http.Request, handler Handler) (*http.Response, []byte, error) {
  33. return handler(ctx, req)
  34. }
  35. }

多个拦截器在执行时,首先会合并为一个拦截器(通过函数 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消息体

如何注册拦截器?

  1. // 通过 Interceptors 设置拦截器
  2. f := fetch.New(ts.URL,
  3. fetch.Debug(true),
  4. fetch.Interceptors(
  5. // fetch.LogInterceptor 会输出以下日志内容
  6. // 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
  7. fetch.LogInterceptor(&fetch.LogInterceptorRequest{
  8. ExcludeReqHeader: nil,
  9. MaxReqBody: 0,
  10. MaxRespBody: 0,
  11. Logger: func(ctx context.Context, format string, args ...interface{}) {
  12. v1, _ := ctx.Value("k1").(string)
  13. log.Printf(format+", extra k1:%v", append(args, v1)...)
  14. },
  15. }),
  16. ))