req

package module
v0.0.0-...-0dc8ad4 Latest Latest
Warning

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

Go to latest
Published: Apr 13, 2025 License: MIT Imports: 17 Imported by: 1

README

req

✨ 通过结构体发送请求 ✨

如何实现一个 API

在正式开始使用前,我们需要了解如何实现一个 API 接口。

// API 信息
type APIData interface {
	RawURL() string
	Method() string
}

// API 构造器
type APICreator interface {
	NewRequestWithContext(ctx context.Context, cli *Client, api APIData) (*http.Request, error)
}

// API 接口
type API interface {
	APIData
	APICreator
}

interfaces.go 中定义了 APIData APICreator API 三个接口。

其中 APIData 是用来描述接口的,包括返回请求地址的 RawURL() string 方法和返回请求方式的 Method() string 方法,通常需要用户自行实现。

此外 APICreator 是用来构造底层 *http.Request 请求的构造器,一般不需用户自己实现。

// GET 请求构造器
//
// 直接嵌入结构体即可使用
type Get struct{}

func (Get) Method() string {
	return http.MethodGet
}

// 可以通过这个方法学习如何自己实现一个构造器
func (Get) NewRequestWithContext(ctx context.Context, cli *Client, api APIData) (req *http.Request, err error) {
	// 因为是 GET 请求所以不添加 body
	req, err = cli.AddBody(ctx, api, nil)
	if err != nil {
		return
	}
	// 提取 API 中字段
	task := LoadTask(api)
	// 获取 API 的值(reflect.Value)以便后续添加参数
	value := reflect.Indirect(reflect.ValueOf(api))
	// 添加请求参数
	err = cli.AddQuery(req, task.Query, value)
	if err == nil {
		// 添加请求头
		err = cli.AddHeader(req, task.Header, value)
	}
	return
}

var _ APICreator = Get{}

method.go 中定义了 Get 结构体,它就实现了 Method() string 方法和 APICreator 接口。如果现在难以理解可以先跳过,我们会在后面详细介绍。

使用

带有路径参数的 GET 请求实例
type User struct {
	req.Get
	UID string
}

func (u User) RawURL() string {
	return "https://httpbin.org/anything/user/" + u.UID
}

func TestUser(t *testing.T) {
	resp, err := req.Do(User{UID: "114514"})
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()

	b, err := io.ReadAll(resp.Body)
	if err != nil {
		t.Fatal(err)
	}
	t.Log(string(b))
}

在上面代码中,我们定义了一个 User 结构体,并在其中嵌入了 req.Get 字段,代表它隐形实现了 Method() string 方法和 APICreator 接口,因此我们只需要实现 RawURL() string 方法。

因此我们在返回请求地址时拼接了地址和路径参数,并且在后续使用时利用 User{UID: "114514"} 填入了参数。

接在在测试代码中使用了 func req.Do(api req.API) (*http.Response, error) 函数,它接收一个 API 并返回请求结果,我们使用命令 go test -v -run ^TestUser$ 在屏幕上得到结果。

> go test -v -run ^TestUser$
=== RUN   TestUser
    req_test.go:31: {
          "args": {},
          "data": "",
          "files": {},
          "form": {},
          "headers": {
            "Accept-Encoding": "gzip",
            "Host": "httpbin.org",
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.54"
          },
          "json": null,
          "method": "GET",
          "origin": "xxx.xxx.xxx.xxx",
          "url": "https://httpbin.org/anything/user/114514"
        }

--- PASS: TestUser (1.18s)
PASS
ok      github.com/Drelf2018/req/tests  1.438s

可以看到,代码成功发送了我们设置了路径参数为 114514GET 请求。

带有 Query Body Header 参数的 POST 请求示例
type Sign struct {
	req.PostForm
	UID           int    `api:"query"`
	Sign          string `api:"body"`
	RefreshNow    bool   `api:"body"`
	Authorization string `api:"header"`
}

func (Sign) RawURL() string {
	return "https://httpbin.org/post"
}

func TestSign(t *testing.T) {
	i, err := req.JSON(Sign{
		UID:           114514,
		Sign:          "逸一时误一世",
		RefreshNow:    true,
		Authorization: "Token 1919810",
	})
	if err != nil {
		t.Fatal(err)
	}

	b, err := json.MarshalIndent(i, "", "  ")
	if err != nil {
		t.Fatal(err)
	}
	t.Log(string(b))
}

在上面代码中,我们定义了一个 Sign 结构体,并在其中嵌入了 req.PostForm 字段,这个字段与 req.Get 类似。

同时还添加了 UID Sign RefreshNow Authorization 四个字段,它们都带有 api 标签。这是本库定义的用来描述某个字段归属的标签,具体来说,这个标签目前支持 query body header file files 五个值,含义如其名。

接在在测试代码中使用了 func req.JSON(api req.API) (any, error) 函数,它接收一个 API 并返回请求结果经 JSON 反序列化的结果,我们使用命令 go test -v -run ^TestSign$ 在屏幕上得到结果。

> go test -v -run ^TestSign$
=== RUN   TestSign
    req_test.go:61: {
          "args": {
            "uid": "114514"
          },
          "data": "",
          "files": {},
          "form": {
            "refresh_now": "true",
            "sign": "逸一时误一世"
          },
          "headers": {
            "Accept-Encoding": "gzip",
            "Authorization": "Token 1919810",
            "Content-Length": "76",
            "Content-Type": "application/x-www-form-urlencoded",
            "Host": "httpbin.org",
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.54",
          },
          "json": null,
          "origin": "xxx.xxx.xxx.xxx",
          "url": "https://httpbin.org/post?uid=114514"
        }
--- PASS: TestSign (1.09s)
PASS
ok      github.com/Drelf2018/req/tests  1.314s

可以看到,代码成功发送了我们设置了 Queryuid=114514FormBodysign=逸一时误一世refresh_now=trueHeaderAuthorization=Token 1919810Content-Type=application/x-www-form-urlencodedPOST 请求。

参数名怎么来的

我们发现请求中参数名与字段名并不完全相同 UID => uid RefreshNow => refresh_now Authorization => Authorization

var (
	nameReplacer   *strings.Replacer
	headerReplacer *strings.Replacer
)

func init() {
	oldnew1 := []string{"ID", "_id"}
	for i := 'A'; i <= 'Z'; i++ {
		oldnew1 = append(oldnew1, string(i)+"ID", "_"+string(i+32)+"id", string(i), "_"+string(i+32))
	}
	nameReplacer = strings.NewReplacer(oldnew1...)

	oldnew2 := make([]string, 0, 26*2)
	for i := 'A'; i <= 'Z'; i++ {
		oldnew2 = append(oldnew2, string(i), "-"+string(i))
	}
	headerReplacer = strings.NewReplacer(oldnew2...)
}

// 一般字段名替换器
func NameReplace(s string) string {
	return nameReplacer.Replace(s)[1:]
}

// 请求头字段名替换器
func HeaderReplace(s string) string {
	return headerReplacer.Replace(s)[1:]
}

这是因为在 replacer.go 中定义了两种生成字段名的函数,其中 HeaderReplace 会用来替换所有带有 api:"header" 标签的字段名,剩下的由 NameReplace 处理。

通过阅读代码可以看出,HeaderReplace 会将所有大写字母 X 替换成 -X 这很符合请求头键名的规范,再由函数 HeaderReplace 做一个截取,去掉最前面的 - 。例如字段名 ContentType => -Content-Type => Content-Type

NameReplace 会将所有 ID 替换成 _id 或者形如 XID 的替换成 _xid 或者 X 替换成 _x ,再由函数去掉最前面的 _ 。例如字段名 UID => _uid => uid RefreshNow => _refresh_now => refresh_now

type Upload struct{
	HTML string `api:"body" req:"html"`
}

如果你对自动生成的字段名不满意,例如 HTML => _h_t_m_l => h_t_m_l ,可以使用 req 标签强制使用该名称。

参数值怎么来的

字段 UID 的类型是 int 为什么成功以字符串写入了 queryRefreshNow 的类型是 bool 为什么成功以字符串写入了 FormBody

这是因为在 marshal.go 中定义了 func Marshal(i any) (string, error) 函数。通过这个函数可以将任意常见类型转换成字符串。此外,对于实现了 req.Marshaler json.Marshaler 接口的值,也会被转换。最终,在没有任意一个类型匹配成功时,会直接对其进行 JSON 序列化得到字符串。

func Marshal(i any) (string, error) {
	if i == nil {
		return "", nil
	}
	switch i := i.(type) {
	case Marshaler:
		return i.MarshalString()
	case json.Marshaler:
		b, err := i.MarshalJSON()
		return string(b), err
	case ...:
		// 省略部分类型 详见文件
	default:
		b, err := json.Marshal(i)
		return string(b), err
	}
}

拓展

interfaces.go 中定义了 CookieJar 接口。直接将实现了此接口的类型嵌入 API 中,并且在发起请求前为其赋值,程序会自动读取这个 CookieJar

// 可判断有效性的 CookieJar
type CookieJar interface {
	IsValid() bool
	http.CookieJar
}
客户端 Client

之前在发送请求时使用了 req.Do 函数,实际上这个函数内部调用 req.DefaultClientfunc (c *Client) Do(api API) (*http.Response, error) 方法。

// 发送请求
func Do(api API) (*http.Response, error) {
	return DefaultClient.Do(api)
}

client.go 中定义了客户端 Client 的结构体。

// 客户端
type Client struct {
	http.Client
	BaseURL   *url.URL
	Header    http.Header
	Variables map[string]any
}

其中 http.Client 即每次发起请求时使用的客户端。

代码中的 RetryTimer 是定义在 interfaces.go 中用来重试请求的接口。

具体实现参考 retry.go 中的 DoubleTimer 。直接将该类型嵌入 API 结构体即可使用。

// 发送带上下文的请求
func (c *Client) DoWithContext(ctx context.Context, api API) (*http.Response, error) {
	req, err := api.NewRequestWithContext(ctx, c, api)
	if err != nil {
		return nil, err
	}
	// 这便是上面提到的自动添加 CookieJar 的实现
	if jar, ok := api.(CookieJar); ok && jar.IsValid() {
		cli := c.Client
		cli.Jar = jar
		resp, err := cli.Do(req)
		if ticker, ok := api.(RetryTicker); ok {
			for i := 0; err != nil; i++ {
				d, ok := ticker.NextRetry(i)
				if !ok {
					break
				}
				time.Sleep(d)
				resp, err = cli.Do(req)
			}
		}
		return resp, err
	}
	resp, err := c.Client.Do(req)
	if ticker, ok := api.(RetryTicker); ok {
		for i := 0; err != nil; i++ {
			d, ok := ticker.NextRetry(i)
			if !ok {
				break
			}
			time.Sleep(d)
			resp, err = c.Client.Do(req)
		}
	}
	return resp, err
}

其中 BaseURL 即每次发起请求时使用基地址,需要使用的 APICreator 使用了这个方法。

// 拼接 BaseURL 和提供的 rawURL
//
// 当 rawURL 以 "/" 开头时才会拼接
func (c *Client) URL(rawURL string) string {
	if c.BaseURL != nil && strings.HasPrefix(rawURL, "/") {
		rawURL = c.BaseURL.JoinPath(rawURL).String()
	}
	return rawURL
}

其中 Header 即每次发起请求时使用基础请求头,需要使用的 APICreator 使用了这个方法。

// 向 req 请求添加请求头
//
// 会使用 Client 中设置的默认请求头
func (c *Client) AddHeader(req *http.Request, header []Field, val reflect.Value) (err error) {
	if c.Header != nil {
		req.Header = c.Header.Clone()
	}
	for _, data := range header {
		req.Header[data.Name] = []string{}  // 覆写基础请求头
		err = c.AddValue(req.Header, data, val)
		if err != nil {
			return
		}
	}
	return
}

其中 Variables 是一个用户可自行添加值的字典,它的用处在下面会介绍。

// 获取 Variables 中的值
//
// 参数 key 必须以 "$" 开头
func (c *Client) Value(key string) any {
	if c.Variables == nil {
		return nil
	}
	if strings.HasPrefix(key, "$") {
		return c.Variables[key]
	}
	return nil
}

// 获取 Variables 中的值并转换成字符串
func (c *Client) ValueString(key string) (string, error) {
	i := c.Value(key)
	if i != nil {
		return Marshal(i)
	}
	return key, nil
}
标签 api 还能怎么用

其实,标签的完全格式为 api:(query|body|header|file|files)[:value][,omitempty]

当标签使用 file files 时,会对该字段类型进行特殊判断。采用 file 时会判断该字段是否实现了 io.Reader 接口,采用 files 时会判断该字段是否是列表或数组,并且元素的类型实现了 io.Reader 接口。本质是用来上传文件的,具体使用方法后面介绍。

标签后面的 [:value] [,omitempty] 这两个是互斥的,分别代表“当前字段值为空时要使用的默认值”和“当前字段值为空时忽略该字段”。字段判空使用的是 func (v reflect.Value) IsZero() bool 方法。

“忽略该字段”很好理解不做多解释,例如 api:"query,omitempty"

“默认值”指使用了类似 api:"query:114" api:"body:$secret" 标签时。如果“默认值”不以 $ 开头,则会将该字符串直接写入这个参数值中。否则,会在 Client 中查找这个名称代表的值,如果没找到则会将带有 $ 的这个字符串写入参数值。

if field.IsZero() {
	if data.Omitempty {
		return nil
	}
	if data.Value != "" {
		s, err := c.ValueString(data.Value)
		if err != nil {
			return err
		}
		adder.Add(data.Name, s)
		return nil
	}
}
怎么上传文件

writer.go 中定义了 FileWriter 接口。

// 文件写入器
type FileWriter interface {
	Adder
	io.Closer

	// 初始化函数 可以做一些赋值操作
	Initial() error

	// 获取最终请求体
	Reader() io.Reader

	// 请求头 Content-Type
	FormDataContentType() string

	// 写入一个文件
	Write(file io.Reader, data Field) error
}

同时定义了一个具体实现 DefaultFileWriter

// 命名的读取器
type NamedReader interface {
	io.Reader
	Name() (filename string)
}

// 写入文件
func (w *DefaultFileWriter) Write(file io.Reader, data Field) error {
	switch file := file.(type) {
	case NamedReader:
		return WriteFormFile(w.Writer, filepath.Join(data.Name, file.Name()), file.Name(), file)
	default:
		return WriteFormFile(w.Writer, filepath.Join(data.Name, data.Value), data.Value, file)
	}
}

可以看到,之前经过判断实现了 io.Reader 的字段会被当做 file io.Reader 参数传入该方法,然后再写入请求。

type file struct {
	name  string
	value string
}

func (f file) Name() string {
	return f.name
}

func (f file) Read(p []byte) (n int, err error) {
	return copy(p, []byte(f.value)), io.EOF
}

var _ req.NamedReader = file{}

type Upload struct {
	req.PostMultipartForm
	FileA  file   `api:"file"`
	FileB  file   `api:"file" req:"file_c"`
	FilesC []file `api:"files"`
	FilesD []file `api:"files" req:"upload/files_e"`
}

func (Upload) RawURL() string {
	return "https://httpbin.org/post"
}

var _ req.API = Upload{}

func TestPostMultipartForm(t *testing.T) {
	var data = Upload{
		FileA:  file{"a.txt", "hello A!"},
		FileB:  file{"b.txt", "hello B!"},
		FilesC: []file{ {"c1.txt", "hello C1!"}, {"c2.txt", "hello C2!"} },
		FilesD: []file{ {"d1.txt", "hello D1!"}, {"d2.txt", "hello D2!"} },
	}
	r, err := cli.JSON(data)
	if err != nil {
		t.Fatal(err)
	}
	r2 := r.(map[string]any)
	for key, value := range r2["files"].(map[string]any) {
		t.Logf("%v: %v", key, value)
	}
}
> go test -v -run ^TestPostMultipartForm$
=== RUN   TestPostMultipartForm
    method_test.go:128: upload\files_e\d1.txt: hello D1!
    method_test.go:128: upload\files_e\d2.txt: hello D2!
    method_test.go:128: file_a\a.txt: hello A!
    method_test.go:128: file_c\b.txt: hello B!
    method_test.go:128: files_c\c1.txt: hello C1!
    method_test.go:128: files_c\c2.txt: hello C2!
--- PASS: TestPostMultipartForm (1.06s)
PASS
ok      github.com/Drelf2018/req/tests  1.299s
接口 APICreator 到底是个啥

直接从 method.go 找出我写好了的一个文件上传请求的构造器。

// 带有文件的请求体的 POST 请求构造器
type PostMultipartForm struct {
	FileWriter
}

func (PostMultipartForm) Method() string {
	return http.MethodPost
}

// 实现 APICreator 的方法 NewRequestWithContext 接收一个上下文 context.Context 一个客户端 *Client 以及一个 API 信息接口 APIData
func (p PostMultipartForm) NewRequestWithContext(ctx context.Context, cli *Client, api APIData) (req *http.Request, err error) {
	// 根据 API 加载任务
	task := LoadTask(api)
	// 获取 API 的值(reflect.Value)以便后续添加参数
	value := reflect.Indirect(reflect.ValueOf(api))
	// 判断当前有没有加载任意一种文件写入器
	if p.FileWriter == nil {
		// 使用默认的文件写入器 封装后的 *multipart.Writer
		p.FileWriter = &DefaultFileWriter{}
	}
	// 初始化写入器
	err = p.FileWriter.Initial()
	if err != nil {
		return
	}
	// 遍历 API 中的 file files 标签的字段
	var field reflect.Value
	for _, data := range task.Files {
		// 找到对应的值
		field, err = value.FieldByIndexErr(data.Index)
		if err != nil {
			return
		}
		// 为空直接跳过
		if field.IsZero() {
			continue
		}
		// 如果是 files 标签就说明有很多文件
		if field.Kind() == reflect.Slice || field.Kind() == reflect.Array {
			for i := 0; i < field.Len(); i++ {
				// 逐一写入文件写入器
				// 因为这些值在前在已经判断过是否实现 io.Reader 所以可以直接断言
				err = p.Write(field.Index(i).Interface().(io.Reader), data)
				if err != nil {
					return
				}
			}
		} else {
			// 如果是 file 标签就只写本身
			err = p.Write(field.Interface().(io.Reader), data)
			if err != nil {
				return
			}
		}
	}
	// 除了文件还要写一些常规的键值对
	for _, data := range task.Body {
		err = cli.AddValue(p, data, value)
		if err != nil {
			return
		}
	}
	// 关闭写入 等待读取 body
	err = p.Close()
	if err != nil {
		return
	}
	// 构建底层请求 *http.Request
	req, err = cli.AddBody(ctx, api, p.Reader())
	if err == nil {
		// 添加常规请求参数
		err = cli.AddQuery(req, task.Query, value)
		if err == nil {
			// 先添加请求头
			err = cli.AddHeader(req, task.Header, value)
			// 在对其覆写
			req.Header.Set("Content-Type", p.FormDataContentType())
		}
	}
	return
}

var _ APICreator = PostMultipartForm{}
JSON 格式响应体导出对应的结构体

client.go 中有 (*Client).Generate 方法,它会请求这个 API 并将返回结果以 JSON 格式解析,如果成功会再将该 JSON 转成 go 语言的结构体形式,最后追加写入给定文件中。该功能为实验性功能,不多作介绍,如想了解详情请看 converter.go 源码。

func TestGenerate(t *testing.T) {
	req.Generate("req_test.go", User{UID: "114514"})
}

// AutoGenerate ↓↓↓

type UserResponse struct {
	Args struct {
	} `json:"args"`
	Data  string `json:"data"` // ""
	Files struct {
	} `json:"files"`
	Form struct {
	} `json:"form"`
	Headers struct {
		AcceptEncoding string `json:"Accept-Encoding"` // "gzip"
		Host           string `json:"Host"`            // "httpbin.org"
		UserAgent      string `json:"User-Agent"`      // "Mozilla/5.1 (Windows NT 10.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.1.1.1 Safari/537.36 Edg/116.1.1938.54"
	} `json:"headers"`
	JSON   any    `json:"json"`
	Method string `json:"method"` // "GET"
	Origin string `json:"origin"` // "xxx.xxx.xxx.xxx"
	URL    string `json:"url"`    // "https://httpbin.org/anything/user/114514"
}

func GetUser() (result UserResponse, err error) {
	err = cli.Result(User{}, &result)
	return
}

Documentation

Index

Constants

View Source
const UserAgent string = "" /* 129-byte string literal not displayed */

Variables

View Source
var DefaultClient = &Client{
	Header: http.Header{
		"User-Agent": {UserAgent},
	},
}
View Source
var ErrDuration = errors.New("req: time.Duration of ForeverTicker must be positive")

Functions

func Content

func Content(api API) ([]byte, error)

获取请求结果

func ContentWithContext

func ContentWithContext(ctx context.Context, api API) ([]byte, error)

获取带上下文的请求结果

func Do

func Do(api API) (*http.Response, error)

发送请求

func DoWithContext

func DoWithContext(ctx context.Context, api API) (*http.Response, error)

发送带上下文的请求

func HeaderReplace

func HeaderReplace(s string) string

请求头字段名替换器

func JSON

func JSON(api API) (any, error)

将请求结果以 JSON 格式解析进接口

func JSONWithContext

func JSONWithContext(ctx context.Context, api API) (any, error)

将带上下文的请求结果以 JSON 格式解析进接口

func JoinPath

func JoinPath(u *url.URL, elem ...string) *url.URL

JoinPath returns a new [URL] with the provided path elements joined to any existing path and the resulting path cleaned of any ./ or ../ elements. Any sequences of multiple / characters will be reduced to a single /.

func Marshal

func Marshal(i any) (string, error)

小白马希洛

func NameReplace

func NameReplace(s string) string

一般字段名替换器

func PreloadTask

func PreloadTask(api ...API)

预加载任务

func Result

func Result[T any](api API) (result T, err error)

将请求结果以 JSON 格式解析进对象

func ResultWithContext

func ResultWithContext[T any](ctx context.Context, api API) (result T, err error)

将带上下文的请求结果以 JSON 格式解析进对象

func Text

func Text(api API) (string, error)

获取请求结果字符串

func TextWithContext

func TextWithContext(ctx context.Context, api API) (string, error)

获取带上下文的请求结果字符串

func TypePtr

func TypePtr(in any) uintptr

func ValuePtr

func ValuePtr(in any) uintptr

ValuePtr can obtain the uintptr of a type from its reflect.Type

TypePtr(something{}) is equal to ValuePtr(reflect.TypeOf(something{}))

func WithRetry

func WithRetry(r RetryTicker) (<-chan int, context.CancelFunc)

WithRetry 可以将重试器转换成一个定时返回当前重试次数的通道

func TestRetry(t *testing.T) {
	resp, err := GetStatus()
	if err != nil {
		c, cancel := req.WithRetry(req.DefaultRetryTicker)
		for range c {
			resp, err = GetStatus()
			if err == nil {
				cancel()
			}
		}
	}
	t.Log(resp.Status)
}

func Write

func Write(api API, name string, perm os.FileMode) error

将请求结果写入文件

func WriteWithContext

func WriteWithContext(ctx context.Context, api API, name string, perm os.FileMode) error

将带上下文的请求结果写入文件

Types

type API

type API interface {
	RawURL() string
	Method() string
}

API 接口

type APIBody

type APIBody interface {
	Body(cli *Client, body []Field, value reflect.Value, api API) (io.Reader, error)
}

API 请求体

type APIHeader

type APIHeader interface {
	Header(req *http.Request, cli *Client, header []Field, value reflect.Value, api API) error
}

API 请求头

type APIQuery

type APIQuery interface {
	Query(req *http.Request, cli *Client, query []Field, value reflect.Value, api API) error
}

API 请求参数

type Adder

type Adder interface {
	Add(string, string)
}

可添加接口

type Any

type Any struct {
	Type  unsafe.Pointer
	Value unsafe.Pointer
}

type CheckResponse

type CheckResponse interface {
	CheckResponse(*http.Response) error
}

检验响应

type Client

type Client struct {
	http.Client

	// 基础路径
	// 若 API 路径以 "/" 开头则会拼接在此路径后
	BaseURL *url.URL

	// 默认请求头
	Header http.Header

	// 自定义变量
	// 当字段 tag 中的值以 "$" 开头则会尝试在该字典中查找对应值
	Variables map[string]any
}

客户端

func (*Client) AddHeader

func (c *Client) AddHeader(req *http.Request, header []Field, val reflect.Value) (err error)

向 req 请求添加请求头

会使用 Client 中设置的默认请求头

func (*Client) AddQuery

func (c *Client) AddQuery(req *http.Request, query []Field, val reflect.Value) (err error)

向 req 请求添加请求参数

func (*Client) AddValue

func (c *Client) AddValue(adder Adder, data Field, v reflect.Value) error

向 Adder 接口添加数据

func (*Client) Authorization

func (c *Client) Authorization() string

获取默认请求头 Authorization

func (*Client) Content

func (c *Client) Content(api API) (p []byte, err error)

获取请求结果

func (*Client) ContentWithContext

func (c *Client) ContentWithContext(ctx context.Context, api API) (p []byte, err error)

获取带上下文的请求结果

func (*Client) Do

func (c *Client) Do(api API) (*http.Response, error)

发送请求

func (*Client) DoWithContext

func (c *Client) DoWithContext(ctx context.Context, api API) (resp *http.Response, err error)

发送带上下文的请求

func (*Client) JSON

func (c *Client) JSON(api API) (data any, err error)

将请求结果以 JSON 格式解析进接口

func (*Client) JSONWithContext

func (c *Client) JSONWithContext(ctx context.Context, api API) (data any, err error)

将带上下文的请求结果以 JSON 格式解析进接口

func (*Client) MakeJSONMap

func (c *Client) MakeJSONMap(body []Field, value reflect.Value) (m map[string]any, err error)

根据提供的 []Field 制作 map[string]any

func (*Client) MakeURLValues

func (c *Client) MakeURLValues(fields []Field, val reflect.Value) (v url.Values, err error)

根据提供的 []Field 制作 url.Values

func (*Client) NewRequest

func (c *Client) NewRequest(api API) (req *http.Request, err error)

新建请求

func (*Client) NewRequestWithContext

func (c *Client) NewRequestWithContext(ctx context.Context, api API) (req *http.Request, err error)

新建带上下文的请求

func (*Client) Result

func (c *Client) Result(api API, result any) error

将请求结果以 JSON 格式解析进对象

result 必须是指针

func (*Client) ResultWithContext

func (c *Client) ResultWithContext(ctx context.Context, api API, result any) (err error)

将带上下文的请求结果以 JSON 格式解析进对象

result 必须是指针

func (*Client) Set

func (c *Client) Set(key string, value any)

设置 Variables 中的值

func (*Client) SetAuthorization

func (c *Client) SetAuthorization(auth string)

设置默认请求头 Authorization

func (*Client) SetUserAgent

func (c *Client) SetUserAgent(val string)

设置默认请求头 User-Agent

func (*Client) Text

func (c *Client) Text(api API) (string, error)

获取请求结果字符串

func (*Client) TextWithContext

func (c *Client) TextWithContext(ctx context.Context, api API) (string, error)

获取带上下文的请求结果字符串

func (*Client) URL

func (c *Client) URL(rawURL string) string

拼接 BaseURL 和提供的 rawURL

当 rawURL 以 "/" 开头时才会拼接

func (*Client) UserAgent

func (c *Client) UserAgent() string

获取默认请求头 User-Agent

func (*Client) Value

func (c *Client) Value(key string) any

获取 Variables 中的值

参数 key 必须以 "$" 开头

func (*Client) Write

func (c *Client) Write(api API, name string, perm os.FileMode) error

将请求结果写入文件

func (*Client) WriteWithContext

func (c *Client) WriteWithContext(ctx context.Context, api API, name string, perm os.FileMode) error

将带上下文的请求结果写入文件

type CookieAdder

type CookieAdder struct {
	URL *url.URL
	http.CookieJar
}

func (*CookieAdder) Add

func (c *CookieAdder) Add(key, val string)

type DoubleTicker

type DoubleTicker int

初始重试时间间隔 1 秒 之后每次重试时间间隔翻倍

值表示最大重试次数

func (DoubleTicker) NextRetry

func (t DoubleTicker) NextRetry(num int) (time.Duration, bool)

type FibonacciTicker

type FibonacciTicker [3]time.Duration

斐波那契重试器

前两项为第一次、第二次重试间隔时间,之后按照斐波那契规则返回新间隔时间,第三项为最大单次重试间隔时间

func (*FibonacciTicker) NextRetry

func (t *FibonacciTicker) NextRetry(num int) (time.Duration, bool)

type Field

type Field struct {
	// 字段在结构体中的索引,因为结构体可以嵌套所以有多层
	Index []int

	// 字段要使用的名称,例如 json 中 key 的部分
	Name string

	// 字段的默认值
	Value string

	// 字段为空时是否忽略这项
	Omitempty bool
}

字段

type ForeverTicker

type ForeverTicker time.Duration

永久重试器

func (ForeverTicker) NextRetry

func (t ForeverTicker) NextRetry(int) (time.Duration, bool)

type Get

type Get struct{}

GET 请求构造器

直接嵌入结构体即可使用

func (Get) Method

func (Get) Method() string

type Marshaler

type Marshaler interface {
	MarshalString() (string, error)
}

type PostForm

type PostForm struct{}

以 Form 表单为请求体的 POST 请求构造器

func (PostForm) Body

func (PostForm) Body(cli *Client, body []Field, value reflect.Value, api API) (r io.Reader, err error)

func (PostForm) Header

func (PostForm) Header(req *http.Request, cli *Client, header []Field, value reflect.Value, api API) (err error)

func (PostForm) Method

func (PostForm) Method() string

type PostJSON

type PostJSON struct{}

以 JSON 为请求体的 POST 请求构造器

func (PostJSON) Body

func (PostJSON) Body(cli *Client, body []Field, value reflect.Value, api API) (io.Reader, error)

func (PostJSON) Method

func (PostJSON) Method() string

type RetryTicker

type RetryTicker interface {
	// 下次重试前需要等待的时间
	//
	// 参数 num 代表已重试次数 从 0 开始
	//
	// 返回值 time.Duration 代表需要等待的时间
	//
	// 返回值 bool 代表是否继续重试
	NextRetry(num int) (time.Duration, bool)
}

重试计时器

var DefaultRetryTicker RetryTicker = DoubleTicker(2)

“试”不过三

func ZeroTicker

func ZeroTicker(maxRetries int, whySafe string) RetryTicker

零间隔重试器

你应该知道自己在做什么、为什么这么做、为什么能这样做

var _ RetryTicker = ZeroTicker(2, "trust me!")

type RetryTransport

type RetryTransport struct {
	http.RoundTripper
	RetryTicker
}

重试传输器

func NewRetryTransport

func NewRetryTransport(ticker RetryTicker) *RetryTransport

func (*RetryTransport) RoundTrip

func (t *RetryTransport) RoundTrip(r *http.Request) (resp *http.Response, err error)

type Task

type Task struct {
	Body   []Field
	Query  []Field
	Header []Field
	Cookie []Field
}

任务

包含了 API 结构体中所有有效字段信息

func LoadTask

func LoadTask(api API) *Task

根据 API 加载任务

不存在则新建

func NewTask

func NewTask(api API) *Task

新建任务

type Unwrap

type Unwrap interface {
	Unwrap() error
}

可解包出错误的接口返回值

Jump to

Keyboard shortcuts

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