调用第三方 API 的三件事:分页、限流和幂等

举报
yd_271078175 发表于 2026/11/05 08:32:51 2026/11/05
【摘要】 写工具对接外部平台时,接口文档往往只写了「怎么调通」,没写「调多了会怎样」。分页少了数据、并发打上去被限流、超时重试把一条记录写了两遍,这三类问题几乎每个对接过第三方 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 字节足够看出问题。

五、小结

三件事对应三个问题:分页处理不好会少数据,限流处理不好会被打回,幂等处理不好会多数据。三个都比调通接口难发现,因为都不是第一次运行时会暴露的问题。

写这类工具时我习惯先确认三件事再动手:列表接口的结束条件是什么、限流的阈值和响应头是什么、写操作重复执行会发生什么。文档里没写的,就只能靠一次小流量试探。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。