深蓝黑背景品牌封面:左侧大标题「GET ≠ POST」,副标题「只说参数放哪,就太浅了」;右侧原创装饰——一枚青色向右箭头标注 GET 并带查询串尾巴,一枚紫色向左箭头标注 POST 并携带请求体方块,两箭头相对而行;左上标注「经典题解析 · 网络」,右上标注「Day 02 · 09-30」 图 1:本文封面,点明「参数位置不是本质区别」。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。「GET 和 POST 有什么区别」大概是网络面试出场率最高的题,也是最容易答浅的题——「参数一个在 URL 一个在请求体」说完这一句,面试官的表情通常不会有任何变化。

想象面试现场,面试官的问题就这一句:说说 GET 和 POST 的区别。

第一步,先定调:区别在语义,不在参数位置。

参数放哪只是表面现象。HTTP 语义标准 RFC 9110 给两个方法的定义是:GET 是「请求获取目标资源的当前表示」,本质是只读的取数操作;POST 是「让服务端按自身语义处理请求里携带的数据」,本质是提交动作。一个语义上的差异,才派生出后面所有的工程差异。

第二步,五个维度把差异讲透。

语义与安全性。RFC 9110 把 GET 定义为安全(safe)方法:只读取、不改变资源状态——当然,服务端记日志这类附带动作不算违反安全性。POST 没有这个承诺,它的使命就是触发处理:创建订单、提交表单、调用生成接口。

幂等性。GET 是幂等的:同一请求执行一次和执行 N 次,效果相同。POST 不幂等:重复提交可能重复创建资源。这一条是工程上最值钱的差异——网络超时后客户端要不要自动重试?GET 放心重试,POST 直接重试就可能重复扣费、重复下单。

缓存。RFC 9110 写明 GET 的响应是可缓存的,浏览器、CDN 都可以存下来复用;POST 的响应默认不缓存,除非响应里显式带了缓存指令。这就是为什么静态资源、搜索接口走 GET 能吃到整个缓存体系的红利。

参数位置。GET 的参数习惯放在 URL 查询串里:会进浏览器历史、服务器访问日志、代理日志,且受 URL 长度限制。POST 的参数在请求体里:格式灵活,JSON、表单、二进制都行。注意这只是约定,GET 也能带请求体、POST 也能带查询串,只是实践与基础设施都按约定优化。

安全性错觉。「POST 比 GET 安全」是经典误解:两者都是明文,保密性一模一样,真正的保密靠 HTTPS。POST 只是参数不出现在 URL 里,避免了进历史和日志的尴尬,仅此而已。

五维对比表:语义上 GET 是「获取资源」的只读操作,POST 是「提交数据让服务端处理」;幂等性上 GET 幂等(执行 N 次效果相同),POST 不幂等(重复提交可能重复创建);缓存上 GET 响应可被浏览器与 CDN 缓存,POST 默认不缓存;参数位置上 GET 在 URL 查询串(有长度限制、会进日志与历史记录),POST 在请求体(格式灵活);安全上两者都不加密,「POST 更安全」是错觉,保密靠 HTTPS。底部工程要点:重试 GET 天然安全,重试 POST 必须靠幂等键防重复扣费或重复生成;方法语义以 RFC 9110 为准 图 2:GET 与 POST 的五维对比与重试工程要点。

第三步,把幂等差异变成可运行的实验。

超时重试 POST 会怎样?下面这份代码模拟一个扣款接口:客户端请求后「超时」,于是重试一次——对比无幂等键与有幂等键两种实现下账户被扣几次,用 Node.js 22.15.0 直接运行(node idempotent-retry.js)即可:

'use strict';

// 模拟服务端账户
const account = { balance: 1000, charged: 0 };
const processed = new Set(); // 已处理过的幂等键

// 无幂等保护:每次请求都扣款
function chargeNaive(amount) {
  account.balance -= amount;
  account.charged++;
  return { ok: true, balance: account.balance };
}

// 有幂等键:同一笔请求重复到达,直接返回首次结果
function chargeIdempotent(amount, idempotencyKey) {
  if (processed.has(idempotencyKey)) {
    return { ok: true, balance: account.balance, duplicate: true };
  }
  processed.add(idempotencyKey);
  return chargeNaive(amount);
}

// 模拟客户端:请求后超时,于是重试一次
function clientPays(chargeFn, times) {
  for (let i = 0; i < times; i++) chargeFn();
}

account.balance = 1000; account.charged = 0; processed.clear();
clientPays(() => chargeNaive(100), 2); // 用户只下一单,超时重试变成两次请求
console.log(`无幂等键:下 1 单重试 1 次,实际扣款 ${account.charged} 次,余额 ${account.balance}`);

account.balance = 1000; account.charged = 0; processed.clear();
const key = 'order-20260930-001';
clientPays(() => chargeIdempotent(100, key), 2);
console.log(`有幂等键:下 1 单重试 1 次,实际扣款 ${account.charged} 次,余额 ${account.balance}`);

实测输出如下:

无幂等键:下 1 单重试 1 次,实际扣款 2 次,余额 800
有幂等键:下 1 单重试 1 次,实际扣款 1 次,余额 900

同一笔订单,超时重试在无保护时实打实扣了两次款;带上幂等键后,第二次请求被服务端识别为重复,原样返回首次结果。POST 天生不幂等,所以幂等性要由应用层自己补——客户端生成幂等键、服务端按键去重,是支付、下单、AI 生成类接口的标配。反过来,GET 幂等又安全,客户端、浏览器、代理都可以放心重试,这也是大模型 API 的查询类端点适合 GET、生成类端点必须处理重试语义的原因。

易错点清单。

一,只会背参数位置,讲不出语义、幂等、缓存三个本质差异。二,把「POST 更安全」当结论——两者都明文,保密靠 HTTPS。三,认为 GET 绝对不能带请求体——规范并未禁止,只是实践不友好。四,给 POST 接口加重试却不上幂等键,线上就是重复扣费事故。

回到面试现场,建议的答题节奏:先一句话把参数位置降级为「表面现象」,再按语义、幂等、缓存、参数、安全五个维度展开,最后落到工程要点——重试 GET 天然安全,重试 POST 必须配幂等键。有原理有落点,这题就答出了区分度。

你设计接口时有没有为幂等性专门做过方案?用的是幂等键、唯一约束还是状态机?欢迎分享你的做法。