Resty

resty 是 Go 里常用的 HTTP 客户端库,基于标准库 net/http 封装,主要解决原生 HTTP 请求代码偏繁琐的问题。

适合用于:

  • 调用第三方 API
  • 调用公司内部服务
  • 发送 GET / POST / PUT / DELETE 请求
  • 处理 JSON 请求体和响应体
  • 设置 Header、Query、Cookie、Timeout、Retry 等

GitHub:

https://github.com/go-resty/resty

安装:

go get resty.dev/v3

基础用法

创建 client:

import (
	"time"

	"github.com/go-resty/resty/v2"
)

client := resty.New().
	SetTimeout(10 * time.Second)

发送 GET 请求:

resp, err := client.R().
	Get("https://api.example.com/users")

if err != nil {
	return err
}

fmt.Println(resp.StatusCode())
fmt.Println(resp.String())

GET 带 Query 参数

resp, err := client.R().
	SetQueryParam("keyword", "登录失败").
	SetQueryParam("page", "1").
	SetQueryParam("size", "20").
	Get("https://api.example.com/tickets")

多个 query 参数也可以这样写:

resp, err := client.R().
	SetQueryParams(map[string]string{
		"keyword": "登录失败",
		"page":    "1",
		"size":    "20",
	}).
	Get("https://api.example.com/tickets")

POST JSON

type CreateTicketRequest struct {
	Title       string `json:"title"`
	Description string `json:"description"`
	Priority    string `json:"priority"`
}

reqBody := CreateTicketRequest{
	Title:       "登录失败",
	Description: "用户反馈无法登录",
	Priority:    "P1",
}

resp, err := client.R().
	SetHeader("Content-Type", "application/json").
	SetBody(reqBody).
	Post("https://api.example.com/tickets")

SetBody 可以直接传 struct,resty 会自动转成 JSON。

解析 JSON 响应

type Ticket struct {
	ID       string `json:"id"`
	Title    string `json:"title"`
	Priority string `json:"priority"`
}

var result Ticket

resp, err := client.R().
	SetResult(&result).
	Get("https://api.example.com/tickets/123")

if err != nil {
	return err
}

fmt.Println(result.ID)
fmt.Println(resp.StatusCode())

SetResult(&result) 会把成功响应自动反序列化到结构体里。

设置 Header

resp, err := client.R().
	SetHeader("Authorization", "Bearer your-token").
	SetHeader("Content-Type", "application/json").
	Get("https://api.example.com/users")

也可以给 client 设置公共 Header:

client := resty.New().
	SetHeader("Authorization", "Bearer your-token").
	SetHeader("Content-Type", "application/json")

后续这个 client 发出的请求都会带上这些 Header。

错误处理

resp, err := client.R().
	SetBody(reqBody).
	Post("https://api.example.com/tickets")

if err != nil {
	return err
}

if resp.IsError() {
	return fmt.Errorf("request failed, status=%d, body=%s",
		resp.StatusCode(),
		resp.String(),
	)
}

常用判断:

resp.IsSuccess() // 2xx
resp.IsError()   // 4xx 或 5xx

设置重试

client := resty.New().
	SetTimeout(10 * time.Second).
	SetRetryCount(3).
	SetRetryWaitTime(1 * time.Second).
	SetRetryMaxWaitTime(5 * time.Second)

适合处理网络抖动、临时超时等场景。

推荐封装方式

业务项目里不要到处直接写 resty.New(),可以封装一个 client:

type APIClient struct {
	client *resty.Client
	baseURL string
}

func NewAPIClient(baseURL, token string) *APIClient {
	client := resty.New().
		SetTimeout(10 * time.Second).
		SetHeader("Authorization", "Bearer "+token).
		SetHeader("Content-Type", "application/json")

	return &APIClient{
		client:  client,
		baseURL: baseURL,
	}
}

func (c *APIClient) GetTicket(id string) (*Ticket, error) {
	var result Ticket

	resp, err := c.client.R().
		SetResult(&result).
		Get(c.baseURL + "/tickets/" + id)

	if err != nil {
		return nil, err
	}

	if resp.IsError() {
		return nil, fmt.Errorf("get ticket failed, status=%d, body=%s",
			resp.StatusCode(),
			resp.String(),
		)
	}

	return &result, nil
}

简单结论

resty 的优点是代码少、链式调用直观、JSON 处理方便,适合业务项目里调用 HTTP API。
如果是 SDK 或基础库,可以考虑原生 net/http;如果是普通业务系统,resty 通常心智负担更低。

错误抓取

errresp.IsError() 判断的是两层不同的错误。

err 主要表示:请求有没有成功发出去、有没有拿到 HTTP 响应

比如这些情况会进 err

DNS 解析失败
网络连不上
连接超时
TLS 证书错误
请求被取消
服务端没有返回完整响应

示例:

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

但如果服务端正常返回了 HTTP 响应,即使状态码是 400401500,通常 err 也是 nil

比如服务端返回:

HTTP/1.1 401 Unauthorized
{
  "message": "invalid token"
}

这时请求本身是成功完成的,所以:

err == nil
resp.StatusCode() == 401
resp.IsError() == true

所以 resp.IsError() 是用来判断:HTTP 状态码是不是业务上失败了

if resp.IsError() {
	return fmt.Errorf("request failed, status=%d, body=%s",
		resp.StatusCode(),
		resp.String(),
	)
}

简单理解:

err          = 网络层 / 请求发送层错误
resp.IsError = HTTP 状态码错误,4xx 或 5xx

常见流程:

resp, err := client.R().
	SetResult(&result).
	Get(url)

if err != nil {
	return err
}

if resp.IsError() {
	return fmt.Errorf("request failed, status=%d, body=%s",
		resp.StatusCode(),
		resp.String(),
	)
}

// 这里才认为请求真正成功

举例:

场景errresp.IsError()
网络断了有值不一定有 resp
域名错了有值不一定有 resp
请求超时有值不一定有 resp
200 OKnilfalse
400 参数错误niltrue
401 token 失效niltrue
500 服务端异常niltrue

所以两个都要判断。err 只能说明“HTTP 请求过程是否成功”,不能说明“接口业务是否成功”。