快递查询API

← 技术博客

用 Go 对接快递查询接口:net/http 与 struct 反序列化

2026-07-26 · 阅读约 8 分钟

用 Go 接快递查询接口,靠标准库就够了——net/http 发请求、 encoding/json 解结构,一个第三方依赖都不用装。本文用带 json 标签的 struct 描述返回,把错误当返回值老老实实处理,走完从单件到批量、 再到重试与签收判断的完整链路,代码都能直接编译。

一、准备工作

注册拿到一对密钥(邮箱即可,无需企业认证):pk_ 开头的 API Key 与 sk_ 开头的 Secret Key。密钥别写死在源码里,用环境变量读:

export KUAIDI_KEY=pk_xxx
export KUAIDI_SECRET=sk_xxx

想先看看返回长什么样,首页的在线演示用公开测试密钥就能试跑。

二、定义请求与响应 struct

Go 的优势是编译期结构清晰。把统一信封映射成 struct,用 json 标签对齐字段名。 注意信封层是 isSuccess、轨迹里是 isDelivered,两个布尔字段都带 is 前缀, 写进标签即可。dataerror 用指针,缺省时为 nil

package main

import (
    "bytes"
    "encoding/json"
    "errors"
    "fmt"
    "log"
    "net/http"
    "os"
    "time"
)

type Item struct {
    CourierCode    string `json:"courierCode"`
    TrackingNumber string `json:"trackingNumber"`
}

type TraceRequest struct {
    Items []Item `json:"items"`
}

type Progress struct {
    DateTime    string `json:"dateTime"`
    Location    string `json:"location"`
    Status      string `json:"status"`
    StatusCode  string `json:"statusCode"`
    Description string `json:"description"`
}

type Tracking struct {
    TrackingNumber     string     `json:"trackingNumber"`
    CourierCode        string     `json:"courierCode"`
    CourierName        string     `json:"courierName"`
    DeliveryStatus     string     `json:"deliveryStatus"`
    DeliveryStatusText string     `json:"deliveryStatusText"`
    IsDelivered        bool       `json:"isDelivered"`
    DateDelivered      string     `json:"dateDelivered"`
    DateLastProgress   string     `json:"dateLastProgress"`
    Progresses         []Progress `json:"progresses"`
    QueriedAt          string     `json:"queriedAt"`
}

type APIError struct {
    Code           string `json:"code"`
    Message        string `json:"message"`
    CourierCode    string `json:"courierCode"`
    TrackingNumber string `json:"trackingNumber"`
    Billable       bool   `json:"billable"`
}

type Result struct {
    Success bool      `json:"success"`
    Data    *Tracking `json:"data"`
    Error   *APIError `json:"error"`
}

type Summary struct {
    Total      int `json:"total"`
    Successful int `json:"successful"`
    Failed     int `json:"failed"`
    Billable   int `json:"billable"`
}

type TraceResponse struct {
    IsSuccess bool `json:"isSuccess"`
    Data      struct {
        Results []Result `json:"results"`
        Summary Summary  `json:"summary"`
    } `json:"data"`
}

响应里还有 cachedemoUsage 等字段没写进 struct 也不影响—— encoding/json 会自动忽略多出来的键。

三、发起请求

http.NewRequest 构造 POST,认证放请求头:两段密钥用英文冒号拼成 Bearer pk_xxx:sk_xxx。这里定义一个携带状态码的错误类型 statusError, 方便后面区分 4xx 与 5xx:

const apiURL = "https://api.kuaidichaxunapi.com/v1/tracking/trace"

type statusError struct{ status int }

func (e *statusError) Error() string {
    return fmt.Sprintf("http %d", e.status)
}

func trace(items []Item) (*TraceResponse, error) {
    payload, err := json.Marshal(TraceRequest{Items: items})
    if err != nil {
        return nil, err
    }

    req, err := http.NewRequest(http.MethodPost, apiURL, bytes.NewReader(payload))
    if err != nil {
        return nil, err
    }
    key := os.Getenv("KUAIDI_KEY")       // pk_ 开头
    secret := os.Getenv("KUAIDI_SECRET") // sk_ 开头
    req.Header.Set("Authorization", "Bearer "+key+":"+secret)
    req.Header.Set("Content-Type", "application/json")

    client := &http.Client{Timeout: 10 * time.Second}
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode >= 400 {
        return nil, &statusError{resp.StatusCode}
    }

    var out TraceResponse
    if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
        return nil, err
    }
    return &out, nil
}

items 是数组,一次最多 10 个;includeProgresses 默认为 true, 想省流量可以在请求体里设为 false 只取状态、不取明细。

四、解析 results 与轨迹

Data.Results 与请求顺序一一对应。逐条先看 Success, 成功再读 Data(指针,注意此时非 nil):

func main() {
    resp, err := trace([]Item{
        {CourierCode: "cainiao", TrackingNumber: "CGD0000123456"},
    })
    if err != nil {
        log.Fatal(err)
    }

    for _, r := range resp.Data.Results {
        if r.Success {
            d := r.Data
            fmt.Printf("%s → %s\n", d.TrackingNumber, d.DeliveryStatusText)
            // progresses 最新在前
            for _, p := range d.Progresses {
                fmt.Printf("  %s %s %s\n", p.DateTime, p.StatusCode, p.Description)
            }
        } else {
            e := r.Error
            fmt.Printf("失败: %s %s\n", e.Code, e.Message)
        }
    }
}

DeliveryStatusText 已经是中文(如「已签收」「运输中」),可直接展示; 真正用来做逻辑判断的是归一后的 DeliveryStatus(12 种状态码之一),跨快递公司通用。

五、批量查询与按错误码分支

把多个运单号塞进一个 items 一次查完。批量里某条失败不会让整个请求出错, 逐条判断 Success,再按 Error.Code 分支:

danhao := []Item{
    {CourierCode: "cainiao", TrackingNumber: "CGD0000123456"},
    {CourierCode: "sf", TrackingNumber: "SF9900001234567"},
    {CourierCode: "zto", TrackingNumber: "ZT8100000123456"},
}

resp, err := trace(danhao)
if err != nil {
    log.Fatal(err)
}
fmt.Println("汇总:", resp.Data.Summary) // total / successful / failed / billable

for _, r := range resp.Data.Results {
    if r.Success {
        fmt.Printf("%s → %s\n", r.Data.TrackingNumber, r.Data.DeliveryStatusText)
        continue
    }
    e := r.Error
    switch e.Code {
    case "COURIER_PREPARING":
        fmt.Printf("%s 该快递查询开发中,本次不计费\n", e.TrackingNumber)
    case "NOT_FOUND":
        fmt.Printf("%s 暂无轨迹,稍后重试\n", e.TrackingNumber)
    default:
        fmt.Printf("%s 失败: %s\n", e.TrackingNumber, e.Code)
    }
}

两个错误码值得记住:NOT_FOUND 表示单号有效但暂无轨迹(新单常见),会计费COURIER_PREPARING 表示该公司查询功能仍在开发中,不消耗额度Summary.Billable 是这批实际计费的条数。要查超过 10 个时,按 10 个一组切片:

func traceAll(all []Item) ([]Result, error) {
    var out []Result
    for i := 0; i < len(all); i += 10 {
        end := i + 10
        if end > len(all) {
            end = len(all)
        }
        resp, err := traceRetry(all[i:end])
        if err != nil {
            return nil, err
        }
        out = append(out, resp.Data.Results...)
    }
    return out, nil
}

六、加重试(只对 5xx 退避)

网络抖动或偶发 5xx 用指数退避最稳妥;但 4xx(密钥错、参数缺)重试也没用, 靠前面的 statusError 就能区分,用 errors.As 取出状态码:

func traceRetry(items []Item) (*TraceResponse, error) {
    const maxRetries = 3
    var lastErr error
    for attempt := 0; attempt < maxRetries; attempt++ {
        resp, err := trace(items)
        if err == nil {
            return resp, nil
        }
        var se *statusError
        if errors.As(err, &se) && se.status < 500 {
            return nil, err // 4xx 不重试,直接查密钥和请求体
        }
        lastErr = err
        wait := time.Duration(1<<attempt) * time.Second // 1s, 2s, 4s… 指数退避
        log.Printf("第 %d 次失败,%v 后重试: %v", attempt+1, wait, err)
        time.Sleep(wait)
    }
    return nil, lastErr
}

七、判断「是否已签收」

做「待收货 / 已完成」分类时,用归一状态码判断最通用——不同快递的原始描述千差万别, 终态都会归到 DELIVERED

func isDelivered(r Result) bool {
    // 用归一后的状态码判断,跨快递公司通用
    return r.Success && r.Data.DeliveryStatus == "DELIVERED"
}

// 等价写法:直接用布尔字段 IsDelivered
func isDelivered2(r Result) bool {
    return r.Success && r.Data.IsDelivered
}

一旦某单 IsDelivered 为真,它已到终态,就不必再查,能省下不少额度。

小结

  • 只用标准库:net/http + encoding/json,struct 加 json 标签对齐字段。
  • 认证是一个头:Authorization: Bearer pk_xxx:sk_xxx,密钥放环境变量。
  • 逐条判断 Success;失败看 Error.Code,其中 COURIER_PREPARING 不计费。
  • statusError + errors.As 区分 4xx / 5xx,只对 5xx 做指数退避;超过 10 个按片查询。
  • 签收判断认准 DELIVERED / IsDelivered,终态即可停查。

完整字段与限制见接口文档,各家快递编码见快递公司页。 免费额度每月 10000 次,无需企业认证,够绝大多数 Go 服务起步。