1. 前言
最近在公司做项目时要调用平台提供的大量 openAPI,尽管 golang 的 http 标准库能够满足需求,但为了实现功能需要写很长的代码,读起来也不是很舒服,所以就想在 github 上找找标准库的封装。想起很久前学 python 时学过的 requests 库,抱着试一试的心态搜索了一下,结果居然真的有 golang 版本的同名库。对着 readme 学了下,发现使用方式还真的蛮 gopher 的,作者还写了一篇博客描述了自己的一些设计取舍,也非常有意思。
下面是一个发送 GET 请求时,使用标准库与 requests 对比的例子:
标准库 |
requests |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://example.com", nil) if err != nil { } res, err := http.DefaultClient.Do(req) if err != nil { } defer res.Body.Close() b, err := io.ReadAll(res.Body) if err != nil { } s := string(b)
|
|
1 2 3 4 5
| var s string err := requests. URL("http://example.com"). ToString(&s). Fetch(ctx)
|
|
需要 15+ 行 | 需要 5 行 |
可以看到对比起来,requests 版本的代码还是非常简单清晰的。
2. 源码解读
2.1. fetch 主流程
从上面的代码中可以看到,requests 发起请求时可以用链式调用的方式声明请求中的内容以及如何处理响应,这个链式调用以 requests.URL
开始,经过一系列的配置后,在 Fetch
调用处发起 http 请求。requests.URL
方法的定义非常简单,它构建了一个 Builder
的结构体,并将其 baseurl 字段设置为 requests.URL
方法的入参,而这个结构体的完整定义如下:
1 2 3 4 5 6 7 8 9 10 11 12
| type Builder struct { baseurl string scheme, host string paths []string params []multimap headers []multimap getBody BodyGetter method string cl *http.Client validators []ResponseHandler handler ResponseHandler }
|
事实上,后续的一系列链式调用都是调用的这个结构体的方法,不同的方法用于填充不同的字段,然后在 Fetch
中以这个结构体的各个字段来构建 http.Request
结构并发送出去,而 Fetch
内部其实仅仅是调用了 Request
和 Do
两个方法,前者用于构建 http.Request
结构,后者则用于发送请求并处理响应,这两个函数的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| func (rb *Builder) Request(ctx context.Context) (req *http.Request, err error) { u, err := url.Parse(rb.baseurl) if err != nil { return nil, fmt.Errorf("could not initialize with base URL %q: %w", u, err) } if u.Scheme == "" { u.Scheme = "https" } if rb.scheme != "" { u.Scheme = rb.scheme } if rb.host != "" { u.Host = rb.host } for _, p := range rb.paths { if strings.HasPrefix(p, "/") { u.Path = p } else if curpath := path.Clean(u.Path); curpath == "." || curpath == "/" { u.Path = path.Clean(p) } else { u.Path = path.Clean(path.Join(u.Path, p)) } } if len(rb.params) > 0 { q := u.Query() for _, kv := range rb.params { q[kv.key] = kv.values } u.RawQuery = q.Encode() } var body io.ReadCloser if rb.getBody != nil { if body, err = rb.getBody(); err != nil { return nil, err } } method := http.MethodGet if rb.getBody != nil { method = http.MethodPost } if rb.method != "" { method = rb.method } req, err = http.NewRequestWithContext(ctx, method, u.String(), body) if err != nil { return nil, err } req.GetBody = rb.getBody
for _, kv := range rb.headers { req.Header[http.CanonicalHeaderKey(kv.key)] = kv.values } return req, nil }
func (rb *Builder) Do(req *http.Request) (err error) { cl := http.DefaultClient if rb.cl != nil { cl = rb.cl } res, err := cl.Do(req) if err != nil { return err } defer res.Body.Close()
validators := rb.validators if len(validators) == 0 { validators = []ResponseHandler{DefaultValidator} } if err = ChainHandlers(validators...)(res); err != nil { return err } h := consumeBody if rb.handler != nil { h = rb.handler } if err = h(res); err != nil { return err } return nil }
|
可以看到,在整个发送请求的过程中,Builder 上定义的 getBody、validators、handler 是非常关键的,它们描述了如何发送请求体与如何处理响应,而这正是一个复杂 http 请求中需要处理的事情。requests 提供了一些 helper 函数来处理一些常见的场景。
2.2. BodyGetter
getBody 的类型是 BodyGetter,它的具体定义为 type BodyGetter = func() (io.ReadCloser, error)
,预期最终会返回一个 io.ReadCloser
,这个返回值在 Request
方法中会作为 http.NewRequestWithContext
的 body 参数。
所以为了传递一个具体的 body,我们可以自己实现一个 BodyGetter 函数,然后在构造请求时通过 Builder.Body
函数传递给 requests,但多数场景下我们可以直接使用 requests 封装好的一些 BodyGetter,这些方法在 Builder
结构上分别有对应的 shortcut,内部的实现很简单,都是用内置的 BodyGetter 作为参数调用 Builder.Body
,并按需设置请求头中的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| func (rb *Builder) Body(src BodyGetter) *Builder { rb.getBody = src return rb }
func (rb *Builder) BodyReader(r io.Reader) *Builder { return rb.Body(BodyReader(r)) }
func (rb *Builder) BodyWriter(f func(w io.Writer) error) *Builder { return rb.Body(BodyWriter(f)) }
func (rb *Builder) BodyBytes(b []byte) *Builder { return rb.Body(BodyBytes(b)) }
func (rb *Builder) BodyJSON(v interface{}) *Builder { return rb. Body(BodyJSON(v)). ContentType("application/json") }
func (rb *Builder) BodyForm(data url.Values) *Builder { return rb. Body(BodyForm(data)). ContentType("application/x-www-form-urlencoded") }
|
由于 Builder.Body
并不会检查 Builder.getBody
是否已经有值,所以如果在一次链式调用中重复调用多个设置请求体的方法,那么最终会以最后一次为准。
从上面的代码可以看到,设置请求体的核心逻辑不在 Builder 的方法中,而是各个方法中传递给 Builder.Body
的函数,这些函数可以从入参获取 BodyGetter,具体来说有如下几个:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| func BodyReader(r io.Reader) BodyGetter { return func() (io.ReadCloser, error) { if rc, ok := r.(io.ReadCloser); ok { return rc, nil } return io.NopCloser(r), nil } }
func BodyWriter(f func(w io.Writer) error) BodyGetter { return func() (io.ReadCloser, error) { r, w := io.Pipe() go func() { var err error defer func() { w.CloseWithError(err) }() err = f(w) }() return r, nil } }
func BodyBytes(b []byte) BodyGetter { return func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil } }
func BodyJSON(v interface{}) BodyGetter { return func() (io.ReadCloser, error) { b, err := json.Marshal(v) if err != nil { return nil, err } return io.NopCloser(bytes.NewReader(b)), nil } }
func BodyForm(data url.Values) BodyGetter { return func() (r io.ReadCloser, err error) { return io.NopCloser(strings.NewReader(data.Encode())), nil } }
|
2.3. ResponseHandler
在 requests 中,validators 和处理请求的 handler 都是 ResponseHandler 类型,这个结构的类型定义为 type ResponseHandler = func(*http.Response) error
,意图也非常明显,就是拿到一个 Response 的指针后对齐做一些处理,如果期间遇到错误就通过返回值抛出。通过这样的函数,requests 允许用户灵活地校验和处理响应体,来适配不同的业务场景。
先说 validators,顾名思义,它的作用是对某个 http 请求返回的内容做一些校验。我们可以通过 Builder.AddValidator
为某个请求加入所需的 validator,这个方法在 Builder.validators
列表中加入一个 ResponseHandler。我们在前面的 Builder.Do
方法中可以看到,在为某个响应执行 handler 之前会先跑一遍所有的 validator,当且仅当全部的 validator 都返回 nil 时才会进一步调用 handler。
而如果没有调用过 AddValidator,那么 validators 列表中就是空的,此时 requests 会默认执行 DefaultValidator,它的定义为:
1 2 3 4 5 6 7
| var DefaultValidator ResponseHandler = CheckStatus( http.StatusOK, http.StatusCreated, http.StatusAccepted, http.StatusNonAuthoritativeInfo, http.StatusNoContent, )
|
进一步来看 CheckStatus 这个函数,它的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12
| func CheckStatus(acceptStatuses ...int) ResponseHandler { return func(res *http.Response) error { for _, code := range acceptStatuses { if res.StatusCode == code { return nil } }
return fmt.Errorf("%w: unexpected status: %d", (*ResponseError)(res), res.StatusCode) } }
|
具体来说,CheckStatus 接收一批 http 状态码作为白名单,当且仅当 Response 中的状态码在这个白名单中时才返回 nil,否则返回一个 error 让 Builder.Do
方法提前返回。除此之外,requests 中还提供 CheckContentType 和 CheckPeek 两种 helper 方法,前者检查响应头中的 content-type 是否在白名单中,后者接收一个函数用来检查响应体的前 n 个字节。
所有的 validators 都通过后,Builder.Do
会执行定义在 Builder 上的 handler 方法,我们可以通过调用 Builder.Handle
方法来设置。如果没有调用过,那么 requests 会默认执行 consumeBody 方法,这个方法的定义如下:
1 2 3 4 5 6 7
| func consumeBody(res *http.Response) (err error) { const maxDiscardSize = 640 * 1 << 10 if _, err = io.CopyN(io.Discard, res.Body, maxDiscardSize); err == io.EOF { err = nil } return err }
|
除此之外,和 BodyGetter 一样,requests 为 handler 也提供了很多内置方法,这些方法在 Builder 上也有对应的 shortcut,具体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| func (rb *Builder) ToJSON(v interface{}) *Builder { return rb.Handle(ToJSON(v)) }
func (rb *Builder) ToString(sp *string) *Builder { return rb.Handle(ToString(sp)) }
func (rb *Builder) ToBytesBuffer(buf *bytes.Buffer) *Builder { return rb.Handle(ToBytesBuffer(buf)) }
func (rb *Builder) ToWriter(w io.Writer) *Builder { return rb.Handle(ToWriter(w)) }
|
而这些 shortcut 内部的 ToXXX 的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| func ToJSON(v interface{}) ResponseHandler { return func(res *http.Response) error { data, err := io.ReadAll(res.Body) if err != nil { return err } if err = json.Unmarshal(data, v); err != nil { return err } return nil } }
func ToString(sp *string) ResponseHandler { return func(res *http.Response) error { var buf strings.Builder _, err := io.Copy(&buf, res.Body) if err == nil { *sp = buf.String() } return err } }
func ToBytesBuffer(buf *bytes.Buffer) ResponseHandler { return func(res *http.Response) error { _, err := io.Copy(buf, res.Body) return err } }
func ToWriter(w io.Writer) ResponseHandler { return ToBufioReader(func(r *bufio.Reader) error { _, err := io.Copy(w, r) return err }) }
|
除此之外,requests 还提供了 ToBufioReader 和 ToBufioScanner,这两者分别接受入参为 *bufio.Reader
和 *bufio.Scanner
的函数,可以从被 requests 注入的入参中持续地读取内容,这对于响应体非常大的请求是非常友好的。
2.4. 其他
除此之外,requests 还允许使用方在 Builder 中设置自定义的 http.Client
,这个结构体可以通过配置内部字段而调整请求的处理流程(RoundTripper),requests 为此还封装了一些常用的 helper 函数,从而让它具备更高的普适性,这部分就不展开说明了,感兴趣的朋友可以自行阅读相关代码(redirects.go、recorder.go、transport.go)。