1. 从一次接口联调说起:为什么curl比Postman更值得练
很多人第一次接触接口调试,都是从图形化工具开始的。点几下、填几个框、选一下请求方法,请求就发出去了,看起来很方便。但我在带新人的过程中发现一个很普遍的现象:用惯了图形化工具的人,一旦换到服务器环境、容器里、或者需要把请求写进脚本自动化执行的时候,立刻就卡住了。因为那些环境里根本没有图形界面,你唯一能依靠的就是命令行工具。
curl就是这样一个绕不开的东西。它几乎预装在所有主流的 Linux 发行版和 macOS 上,Windows 10 之后的系统也自带了 curl。它不依赖任何运行时环境,一条命令就能把 HTTP 请求发出去,还能把响应原封不动地打印出来。对于后端开发、运维、测试来说,curl 是真正意义上的"通用语言"。
这篇文章我聚焦两件最常用、也最容易出错的事:用 curl 发送带 JSON 参数的 POST 请求,以及用 curl 发送带查询参数的 GET 请求。别看这两个动作简单,里面涉及的内容类型声明、引号转义、参数编码、响应处理等细节,坑一点都不少。我会把每个参数为什么这么写、不这么写会出什么问题,都掰开讲清楚。适合刚接触接口调试的新手,也适合想把自己命令行的老手查漏补缺。
先给一个最直观的对比。下面这条是发送 JSON POST 请求的最小可用命令:
curl -X POST https://api.example.com/v1/users \ -H "Content-Type: application/json" \ -d '{"name":"张三","age":28}'看起来就三行,但每一行都有讲究。-X POST指定方法,-H声明内容类型,-d携带请求体。接下来我会逐层拆开,把每个部分背后的逻辑讲透。
2. POST 请求携带 JSON 参数:从最小命令到生产级写法
2.1 Content-Type 为什么必须是 application/json
这是新手最容易忽略、也最容易导致"请求发出去了但服务端报错"的地方。HTTP 请求体本身只是一串字节,服务端怎么知道这串字节该按什么格式解析?答案就在Content-Type请求头里。
如果你不写这个头,curl 默认会带上Content-Type: application/x-www-form-urlencoded。这时候服务端会尝试把你的 JSON 字符串当成表单数据来解析,结果就是字段全部丢失或者解析失败,返回一个 400 或者参数为空的响应。我见过太多次"我明明发了 JSON,为什么后端说没收到参数",九成都是这个头没写对。
所以记住一条铁律:只要请求体是 JSON,就必须显式声明Content-Type: application/json。写法上,-H后面跟的字符串里,冒号后面建议留一个空格,虽然 HTTP 规范允许不留,但留空格可读性更好,也避免某些服务端解析器对格式敏感。
2.2 -d 参数的引号陷阱:单引号、双引号与转义
-d是--data的简写,用来指定请求体内容。这里最大的坑在于 shell 的引号处理机制。
在 Linux/macOS 的 bash 或 zsh 里,单引号内的内容会被原样传递,不做任何变量替换。所以下面这种写法是最安全的:
-d '{"name":"张三","age":28}'JSON 本身用双引号包裹键和字符串值,外层用单引号,两者不冲突,干净利落。
但如果你在 Windows 的 CMD 里,情况就不一样了。CMD 不认单引号作为字符串边界,你得用双引号包裹整个 JSON,然后对内部的每个双引号做转义:
curl -X POST https://api.example.com/v1/users ^ -H "Content-Type: application/json" ^ -d "{\"name\":\"张三\",\"age\":28}"注意 Windows CMD 的换行符是^,不是反斜杠。如果你用的是 PowerShell,转义规则又变了,PowerShell 里反引号是转义符,处理 JSON 会非常痛苦。我的建议是:在 Windows 上做接口调试,优先用 Git Bash 或者 WSL,能省掉大量转义的麻烦。
还有一个隐蔽的坑:如果你的 JSON 里包含单引号(比如某个字段值是it's),那外层就不能用单引号了,得换成双引号并转义内部双引号,或者用$'...'这种 bash 的 ANSI-C 引用方式。这种边界情况在实际业务里并不罕见,尤其是处理英文文本内容的时候。
2.3 从文件读取请求体:-d @filename 的实战价值
当 JSON 参数很长、嵌套层级很深的时候,把它塞在命令行里既难读又容易出错。这时候可以把 JSON 写到一个文件里,然后用@符号引用:
curl -X POST https://api.example.com/v1/orders \ -H "Content-Type: application/json" \ -d @order.jsonorder.json文件内容就是标准的 JSON,不需要任何转义。这个方式在自动化脚本里特别有用,因为你可以用程序动态生成 JSON 文件,再交给 curl 发送,职责分离得很清楚。
这里有个细节要注意:-d @filename会把文件内容原样作为请求体,但它不会自动设置 Content-Type,所以你依然要手动加-H。另外,如果文件名以@开头,需要用--data @./@file这种方式规避歧义。
2.4 用 --data-binary 保留原始字节
-d有一个不太为人知的行为:它会去掉请求体里的换行符。对于大多数紧凑的 JSON 来说无所谓,但如果你发送的是格式化过的多行 JSON,或者发送的是二进制数据、文件上传,-d就会破坏内容。
这时候要用--data-binary:
curl -X POST https://api.example.com/v1/upload \ -H "Content-Type: application/json" \ --data-binary @payload.json--data-binary会原封不动地发送文件内容,包括换行和所有字节。我在做文件哈希校验、签名验证这类对字节敏感的场景时,一律用--data-binary,避免因为换行被吞导致签名对不上。
2.5 一个完整的生产级 POST 示例
把上面的要点组合起来,一个健壮的 POST 请求大概长这样:
curl -X POST "https://api.example.com/v1/users" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" \ --data-binary @user.json \ --connect-timeout 10 \ --max-time 30 \ -w "\nHTTP状态码: %{http_code}\n耗时: %{time_total}s\n" \ -o response.json这里额外加了几个生产环境常用的参数:--connect-timeout控制连接超时,--max-time控制整个请求的最大耗时,避免脚本卡死;-w用来在请求结束后打印状态码和耗时,方便日志记录;-o把响应体写入文件,而不是直接刷屏。这些参数在写自动化脚本时几乎是标配,建议养成习惯。
3. GET 请求的参数拼接:查询字符串的正确姿势
3.1 最朴素的写法与它的隐患
GET 请求把参数放在 URL 的查询字符串里,最基本的写法就是直接把参数拼在 URL 后面:
curl "https://api.example.com/v1/users?page=1&size=20&keyword=test"注意这里整个 URL 用双引号包起来了。为什么?因为 URL 里的&在 shell 里是"后台执行"的意思,不加引号的话,shell 会把&后面的内容当成另一条命令,请求就发不完整了。这是新手最常踩的坑之一,症状是"请求只发了一半参数"或者"命令执行后卡住不动"。
所以第一条经验:只要 URL 里带&,就必须用引号包裹整个 URL。单引号双引号都行,单引号更保险,因为它不做变量替换。
3.2 用 --data-urlencode 配合 -G 自动编码
手动拼接参数有个大问题:特殊字符需要 URL 编码。比如参数值里有空格、中文、&、=、#这些字符,直接拼进去会破坏 URL 结构。空格会被截断,&会被当成参数分隔符,#后面的内容会被当成锚点丢弃。
curl 提供了一个非常优雅的解决方案:用-G把-d的参数转成查询字符串,同时用--data-urlencode自动做 URL 编码:
curl -G "https://api.example.com/v1/search" \ --data-urlencode "keyword=张三 李四" \ --data-urlencode "category=技术&开发" \ --data-urlencode "page=1"-G的作用是告诉 curl:把后面-d系列参数拼成查询字符串,用 GET 方法发送。--data-urlencode会自动把参数值里的特殊字符编码成%XX形式。上面那个keyword=张三 李四会被正确编码成keyword=%E5%BC%A0%E4%B8%89%20%E6%9D%8E%E5%9B%9B,服务端能正确解析。
这个写法我强烈推荐,尤其是参数值来自用户输入或者包含中文的时候。手动编码既费时又容易出错,交给 curl 处理最省心。
3.3 中文参数的编码原理与验证方法
说到中文编码,稍微展开讲一下原理,理解了就不容易慌。URL 里只能出现 ASCII 字符,中文属于非 ASCII,必须先转成字节序列再编码。以 UTF-8 为例,"张"这个字的 UTF-8 字节是E5 BC A0,每个字节前面加%就变成%E5%BC%A0。所以一个中文参数编码后会变得很长,这是正常的。
如果你不确定编码对不对,可以用-v参数看 curl 实际发出的请求:
curl -v -G "https://api.example.com/v1/search" \ --data-urlencode "keyword=张三"-v会打印出完整的请求行,你能看到GET /v1/search?keyword=%E5%BC%A0%E4%B8%89 HTTP/1.1这样的内容,一眼就能确认编码是否正确。这个技巧在排查"服务端收到的中文是乱码"这类问题时特别有用。
3.4 GET 请求也能带请求头
虽然 GET 请求通常不需要请求体,但请求头是照常可以带的。比如需要鉴权、需要指定接受的数据格式:
curl -G "https://api.example.com/v1/users" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" \ --data-urlencode "page=1" \ --data-urlencode "size=20"这里有个容易混淆的点:GET 请求里用-d配合-G时,-d的内容会变成查询字符串,而不是请求体。如果你不加-G,-d就会让 curl 自动把方法变成 POST。所以-G和-d是一对搭档,缺一不可。
4. 响应处理与调试:让 curl 的输出真正有用
4.1 只看响应体、只看响应头、两者都要
curl 默认只输出响应体。但调试的时候,响应头里的状态码、Content-Type、Set-Cookie 往往更关键。控制输出的参数有这么几个:
| 参数 | 作用 | 适用场景 |
|---|---|---|
| 默认 | 只输出响应体 | 拿数据、管道给其他程序 |
-i | 输出响应头 + 响应体 | 需要看状态码和头信息 |
-I | 只输出响应头(发 HEAD 请求) | 快速检查资源是否存在 |
-v | 输出完整通信过程 | 深度排查连接、TLS、重定向问题 |
-s | 静默模式,不显示进度条 | 脚本里用,输出干净 |
我个人的习惯是:日常调试用-i,脚本里用-s,遇到诡异问题用-v。-v的输出会包含>开头的发送内容和<开头的接收内容,能清楚看到 curl 到底发了什么、服务端回了什么,是排查问题的终极武器。
4.2 用 -w 定制输出格式,把状态码和耗时打出来
-w(write-out)是 curl 里被低估的一个功能。它允许你在请求结束后,按自定义格式打印一些元信息。常用的变量有:
%{http_code}:HTTP 状态码%{time_total}:总耗时(秒)%{time_connect}:TCP 连接耗时%{size_download}:下载字节数%{content_type}:响应的 Content-Type
一个实用的组合:
curl -s -o /dev/null -w "状态码:%{http_code} 耗时:%{time_total}s 大小:%{size_download}字节\n" \ "https://api.example.com/v1/health"-o /dev/null把响应体丢弃,只保留-w的输出。这条命令特别适合做健康检查或者接口性能摸底,输出干净利落,方便脚本解析。
4.3 处理重定向与超时
默认情况下,curl 不会跟随 3xx 重定向,只会把重定向响应原样返回。如果你希望它自动跟随,加-L:
curl -L "https://api.example.com/v1/redirect-endpoint"但要注意,-L在 POST 请求上有个坑:当重定向目标是 301/302 时,很多客户端会把方法改成 GET,请求体也会丢失。curl 的行为取决于具体版本和--post301、--post302这些参数。如果你的接口依赖重定向后仍保持 POST,务必显式加上--post301 --post302 --post303。
超时方面,前面提过--connect-timeout和--max-time。我的经验值是:连接超时设 5 到 10 秒,总超时根据接口正常响应时间乘以 3 来设。比如一个正常 200ms 返回的接口,总超时设 5 秒就够,设太长反而会让故障排查变慢。
4.4 把响应交给 jq 做格式化
服务端返回的 JSON 通常是一整行,人眼很难读。这时候用jq管道处理一下,立刻清爽:
curl -s "https://api.example.com/v1/users?page=1" | jq .jq .会把 JSON 格式化并高亮。如果只想取某个字段:
curl -s "https://api.example.com/v1/users/1" | jq '.data.name'这个组合在调试接口时几乎是必备的。jq需要单独安装,主流包管理器里都有,装一次受用很久。
5. 那些年我踩过的 curl 坑:一份排错清单
5.1 参数丢失、乱码、请求方法不对
这三个问题占了 curl 使用问题的绝大多数,我把它们整理成一张对照表,遇到时直接查:
| 现象 | 最可能的原因 | 解决办法 |
|---|---|---|
| 服务端收不到参数 | Content-Type 没设或设错 | 加-H "Content-Type: application/json" |
| 中文变乱码 | 没做 URL 编码 | 用--data-urlencode配合-G |
| 请求只发了一半 | URL 里的&没被引号包裹 | 整个 URL 用引号包起来 |
| GET 变成了 POST | 用了-d但没加-G | 加-G参数 |
| JSON 解析失败 | 引号转义错误 | 外层用单引号,或改用@file |
| 请求体换行丢失 | 用了-d而非--data-binary | 换成--data-binary |
| 命令卡住不返回 | 服务端没响应且无超时 | 加--max-time |
这张表我建议截图存下来,遇到问题先对照一遍,能省下大量搜索时间。
5.2 关于引号,再强调一次
引号问题是 curl 新手最大的拦路虎,我单独拎出来说。核心规则就三条:
第一,JSON 请求体外层用单引号,因为 JSON 内部全是双引号,用单引号包裹就不用转义。第二,URL 里有&时整个 URL 用引号包裹,否则 shell 会截断。第三,Windows 环境尽量避开 CMD,用 Git Bash 或 WSL,转义规则和 Linux 一致,能少掉一半的头发。
如果你实在要在 CMD 里写复杂 JSON,我的建议是别硬扛,把 JSON 写到文件里用-d @file.json,绕开所有转义问题。这不是偷懒,是聪明的做法。
5.3 认证信息不要硬编码在命令里
调试阶段直接在命令里写 token 很方便,但一旦命令被记录到 shell 历史、日志文件、或者 CI 的构建输出里,token 就泄露了。正确的做法是用环境变量:
curl -X POST "https://api.example.com/v1/users" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer ${API_TOKEN}" \ -d @user.json然后在执行前export API_TOKEN=xxx。更进一步,可以用--netrc-file或者从密钥管理服务动态获取。这个习惯越早养成越好,我见过因为命令历史泄露密钥导致的安全事件,代价很大。
5.4 批量请求时的连接复用
如果你要连续发很多请求,比如压测或者批量同步数据,每次都启动一个新的 curl 进程开销不小。curl 支持在单次调用里发多个 URL:
curl -s "https://api.example.com/v1/users/1" \ -s "https://api.example.com/v1/users/2" \ -s "https://api.example.com/v1/users/3"curl 会复用底层连接,比循环调用快不少。不过要注意,多个 URL 的输出会连在一起,最好配合-o分别写文件,或者用-w加分隔符。如果请求量真的很大,那还是建议用专门的压测工具或者写脚本用编程语言的 HTTP 库,curl 更适合中小规模的批量操作。
6. 把 curl 用进日常:几个真实场景的落地写法
6.1 接口健康检查脚本
运维场景里经常需要定时探测接口是否存活。一个简单的健康检查脚本可以这样写:
#!/bin/bash URL="https://api.example.com/v1/health" CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$URL") if [ "$CODE" = "200" ]; then echo "OK" else echo "FAIL: $CODE" fi这个脚本可以挂到定时任务里,配合告警系统使用。关键点是-o /dev/null丢弃响应体、-w只取状态码、--max-time防止卡死。三个参数缺一不可。
6.2 用 curl 做接口回归测试
在 CI 流程里,可以用 curl 快速验证接口契约。比如验证创建用户接口返回 201 且响应体包含 id 字段:
RESP=$(curl -s -X POST "https://api.example.com/v1/users" \ -H "Content-Type: application/json" \ -d '{"name":"test","age":20}') CODE=$(echo "$RESP" | jq -r '.code') if [ "$CODE" != "0" ]; then echo "接口回归失败: $RESP" exit 1 fi这种写法轻量、无依赖,适合放在部署流水线的最后一步做冒烟测试。比引入一套完整的测试框架要快得多,对于小团队来说性价比很高。
6.3 抓取接口数据做临时分析
有时候需要快速拉一批数据下来看看,用 curl 配合 jq 就能搞定:
for i in $(seq 1 10); do curl -s "https://api.example.com/v1/items?page=$i" | jq -c '.data[]' done > items.jsonl这段脚本把 10 页数据拉下来,每行一个 JSON 对象,存成 JSONL 格式,后续用任何工具都能处理。jq -c是紧凑输出,一行一个对象,方便流式处理。这种临时脚本在数据分析、问题排查时特别顺手。
6.4 关于 curl 版本的一点提醒
curl 的功能在不同版本间有差异,比如--data-urlencode是 7.18 之后才有的,--retry系列参数也是后来加的。如果你在老旧系统上跑脚本,可能会遇到参数不识别的问题。用curl --version确认版本,必要时在脚本里做兼容处理。我一般会在脚本开头加一句版本检查,避免在陌生环境里翻车。
7. 写在最后的一点个人体会
curl 这个工具,刚上手的时候觉得参数多、语法怪,但用熟了之后会发现它的设计其实非常一致:-H管头,-d管体,-X管方法,-G管查询字符串,-o管输出,-w管元信息。把这几个核心参数吃透,日常百分之九十的接口调试场景都能覆盖。
我自己的习惯是,每遇到一个新的接口,先用 curl 在命令行里跑通,确认请求格式、认证方式、响应结构都对了,再把它翻译成代码。这样能把"接口本身的问题"和"代码写得不对的问题"分开,排查起来效率高很多。很多时候代码里调不通,回头用 curl 一试,发现是请求头少了一个,或者参数编码方式不对,问题立刻定位。
最后分享一个小技巧:把常用的 curl 命令存成一个 shell 函数或者别名,比如api-get、api-post,把 token、base url、常用头都封装进去,日常调用就只需要传路径和参数。这样既省事,又能避免每次都手写一长串参数出错。工具的价值就在于把重复劳动固化下来,让注意力集中在真正需要思考的地方。