处理 Response

判断请求是否成功

要判断请求是否成功,首先要看是否发生 error:

resp, err := client.R().Get(url)
if err != nil {
    log.Fatal(err)
}

如果用下面这种风格,也可以通过判断 resp.Err 来判断是否发生 error:

resp := client.Get(url).Do()
if resp.Err != nil {
    log.Fatal(err)
}

任何情况下返回的 “resp” 永远不会是 nil,可以放心的直接判断。

如果没有发生 error,再判断状态码是否异常,一般状态码在 200~299 之间表示是成功响应,可以直接这样判断:

if resp.IsSuccessState() {
	...
}

状态码大于等于 400 时表示服务端响应了错误,可以直接这样判断:

if resp.IsErrorState() {
  ...
}

其它情况一般是不常见的未知异常。

通常建议使用中间件来统一对所有请求处理异常,避免重复代码,参考 使用中间件统一处理异常

req.Response 与 http.Response

请求发起后返回的是 req.Response,代表 HTTP 响应的结构体,里面嵌入了底层返回的原始的 *http.Response,所以我们可以直接在 req.Response 中访问 http.Response 中的字段,比如判断状态码时,可以将 resp.Response.StatusCode 简写为 resp.StatusCode:

resp, err := client.R().Get(url)
if err != nil {
    log.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
	...
}

当发生 error 的时候,resp.Response 为 nil,这时如果仍要访问 resp.StatusCoderesp.Header 就会导致 panic,在没判断 error 时就想直接判断状态码或 Header,可以用 resp.GetStatusCode()resp.GetHeader(key) 这种更安全的方法。

读取响应体内容为 string 或 []byte

你可以使用 resp.String()resp.Bytes() 来获取被自动读到内存中的响应体内容,不会返回 error (参考 自动读取响应体):

resp, err := client.R().Get(url)
if err != nil {
    log.Fatal(err)
}
body := resp.String()
fmt.Println("body:", body)

如果响应体没有被自动读到内存,可以使用 resp.ToString() or resp.ToBytes() 来获取 (读取出错的话会返回 error):

body, err := resp.ToString()
if err != nil {
    log.Fatal(err)
}
fmt.Println("body:", body)

Unmarshal 响应体

你可以调用 RequestSetSuccessResultSetErrorResult 来让响应体自动 Unmarshal 到 struct 或 map 中去:

resp, err := client.R().
    SetSuccessResult(&result).
    SetErrorResult(&errMsg).
    Get(url)
  • 如果 resp.IsSuccessState() 返回 true, 默认意味着响应状态码在 200~299 ,如果 err 是 nil,也表示响应体一定成功被 Unmarshal 到 &result 里了。
  • 如果 resp.IsErrorState() 返回 true,默认意味着响应状态码大于或等于 400,如果 err 是 nil,也表示响应体一定成功被 Unmarshal 到 &errMsg 里了。

通常我们可以这样处理响应:

if err != nil { // Could be network error or unmarshal error
    // Handle error
    // ...
    return
}
if resp.IsSuccessState() {
    name := result.Name
    // ...
}else if resp.IsErrorState() {
    msg := errMsg.Message
    // ...
}else { // Bad status
    status := resp.Status
    // ...
}

你也可以使用 resp.Unmarshalresp.Into 来显式的进行 Unmarshal:

if resp.IsSuccess() {
    err = resp.Into(&user)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s's blog is %s\n", user.Name, user.Blog)
} else {
    fmt.Println("bad response:", resp)
}

不管是 Request.SetSuccessResultRequest.SetErrorResult 还是 Response.Into,都允许传入空指针的指针,当函数需要根据响应体来返回对应 struct 的指针时,可以直接将函数定义的返回值列表中的指针变量的地址传入(无需创建 struct,内部会自动使用反射来创建),在封装 SDK 时尤为实用:

func (c *GithubClient) GetMyProfile() (user *UserProfile, err error) {
	_, err = c.R().
		SetResult(&user).
		Get("/user")
	return
}
func (c *GithubClient) GetMyProfile() (user *UserProfile, err error) {
err = c.Get("/user").
	Do().
	Into(&user)
return
}

自动读取响应体

默认情况下,如果不是下载的请求,响应体会被自动读入内存,如有需要也可以选择禁用自动读取(通常不需要这样做):

client.DisableAutoReadResponse()

resp, err := client.R().Get(url)
if err != nil {
	log.Fatal(err)
}
io.Copy(dst, resp.Body)

结合 gjson 轻松获取 json 字段

如果返回的响应体是 json 格式,想要获取指定字段的值,但又不想定义 struct 去 Unmarshal 来获取字段的值,可以结合 gjson 来获取指定字段的值。

代码示例:

package main

import (
  "fmt"
  "github.com/imroc/req/v3"
  "github.com/tidwall/gjson"
)

type Response struct {
  *req.Response
}

func (resp Response) GetJsonStringField(path string) string {
  result := gjson.Get(resp.String(), path)
  return result.String()
}

func (resp Response) GetJsonStringArrayField(path string) (ret []string) {
  result := gjson.Get(resp.String(), path)
  for _, item := range result.Array() {
    ret = append(ret, item.String())
  }
  return ret
}

func main() {
  req.EnableDumpAllWithoutRequest()
  resp := Response{req.MustGet("https://httpbin.org/json")}

  topic := resp.GetJsonStringField("slideshow.title")
  slidesTitles := resp.GetJsonStringArrayField("slideshow.slides.#.title")

  fmt.Println("The topic is", topic)
  for i, title := range slidesTitles {
    fmt.Printf("Slide %d is %s\n", i+1, title)
  }
}

运行:

$ go run .
:status: 200
date: Fri, 26 Aug 2022 05:43:15 GMT
content-type: application/json
content-length: 429
server: gunicorn/19.9.0
access-control-allow-origin: *
access-control-allow-credentials: true

{
  "slideshow": {
    "author": "Yours Truly",
    "date": "date of publication",
    "slides": [
      {
        "title": "Wake up to WonderWidgets!",
        "type": "all"
      },
      {
        "items": [
          "Why <em>WonderWidgets</em> are great",
          "Who <em>buys</em> WonderWidgets"
        ],
        "title": "Overview",
        "type": "all"
      }
    ],
    "title": "Sample Slide Show"
  }
}

The topic is Sample Slide Show
Slide 1 is Wake up to WonderWidgets!
Slide 2 is Overview