调用第三方 API 的三件事:分页、限流和幂等
写工具对接外部平台时,接口文档往往只写了「怎么调通」,没写「调多了会怎样」。分页少了数据、并发打上去被限流、超时重试把一条记录写了两遍,这三类问题几乎每个对接过第三方 API 的人都遇到过。
这篇按顺序说这三件事,都是标准库能解决的。
一、分页:默认那一页不是全部
绝大多数列表接口都支持分页,但不分页也能拿到数据,于是很容易写出这样的代码:
var all []Item
page := 1
for {
resp := fetch(page)
all = append(all, resp.Items...)
if len(resp.Items) == 0 {
break
}
page++
}
这段逻辑本身没错,问题在于它把「空页」当成了唯一的结束条件。有两种情况会让它出错:
- 服务端对页码有上限,比如最多返回第 100 页,之后开始返回空页;
- 服务端在数据变动的瞬间出现中间某页为空,后面的页其实还有数据。
更稳的判断是拿返回里的 total 和前面积累的数量比:
for len(all) < resp.Total {
// 继续翻页
}
如果接口返回的是游标(next_cursor、page_token),那就别自己算页码,跟着游标走,直到它为空。游标分页天生不会因为数据插入而漏掉或重复。
还有一个小习惯值得养:跑完以后打一行「总数 / 实际拿到」,对不上就是分页逻辑有问题,比事后发现少了几十条容易得多。
二、限流:429 是约定,不是意外
限流时对端通常返回 429 Too Many Requests,很多实现把它当成普通错误直接抛出去。实际上 429 是最应该单独处理的状态码,因为它往往附带了「什么时候可以再来」:
if resp.StatusCode == http.StatusTooManyRequests {
if v := resp.Header.Get("Retry-After"); v != "" {
if secs, err := strconv.Atoi(v); err == nil {
time.Sleep(time.Duration(secs) * time.Second)
continue
}
}
}
Retry-After 有两种格式:秒数,或者一个 HTTP 日期。生产环境里两种都见过,解析失败就退回自己的指数退避。
另一方面,限流多半是自己打太急了。给并发加一个上限通常比加重试更有效:
sem := make(chan struct{}, 4)
for _, item := range items {
sem <- struct{}{}
go func(it Item) {
defer func() { <-sem }()
process(it)
}(item)
}
四个并发还是八个,取决于对端文档写的阈值。没写就保守一点,4 是个安全值。
三、幂等:重试之前先问自己会不会写两遍
GET 重试是安全的,POST 不是。超时的时候请求可能已经到了对端、只是响应没回来,这时候重试就会创建两条记录。
有两条路可以走:
查-改代替写。 如果接口支持「按某个业务键查询」,重试前先查一遍,存在就跳过。多一次请求,换来不会重复。
用对端提供的幂等键。 一些接口接受 Idempotency-Key 头,同一次业务操作重试时带同一个值,服务端自己去重:
req.Header.Set("Idempotency-Key", sha256Hex(operationID))
两个都没有的时候,最务实的做法是不自动重试 POST,把失败留给人工判断。宁可少做一次,也别做两次。
四、错误响应不一定是 JSON
这一点很坑:接口正常时返回 JSON,出错时却返回一段 HTML 错误页或者纯文本。代码里上来就 json.Unmarshal 响应体,真正的错误信息(比如网关的 502 页面)就被一条 invalid character '<' 掩盖了。
处理方式是先看状态码,再决定怎么解析:
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
io.LimitReader 是为了防止对端返回一个巨大的错误页面把内存吃满。截前 512 字节足够看出问题。
五、小结
三件事对应三个问题:分页处理不好会少数据,限流处理不好会被打回,幂等处理不好会多数据。三个都比调通接口难发现,因为都不是第一次运行时会暴露的问题。
写这类工具时我习惯先确认三件事再动手:列表接口的结束条件是什么、限流的阈值和响应头是什么、写操作重复执行会发生什么。文档里没写的,就只能靠一次小流量试探。
- 点赞
- 收藏
- 关注作者
评论(0)