用 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 前缀,
写进标签即可。data 与 error 用指针,缺省时为 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"`
}
响应里还有 cache、demoUsage 等字段没写进 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 服务起步。