第 6 章 URL:世界上最成功也最拧巴的一次命名
URL 是人类历史上最成功的一次”命名”:它用一行字符串,同时完成了”这是什么、它在哪、怎么拿到”三件事。但它的成功,恰恰掩盖了它的拧巴。
6.0 引子:改了个 URL,全崩了
小美:老哥,我们把接口路径从 /api/orders 改成了 /api/v2/orders,为什么 SEO 全没了?用户收藏夹里的旧链接也全 404 了?
你:那肯定啊,路径变了,老链接当然失效。
小美:可是功能没变啊,只是加了个版本号……
老张:而且 CDN 缓存全失效了,回源暴涨,带宽又打满了。
墨叔:来了,两个问题,背后是同一个被低估的事实:
URL 不只是”地址”,它是”资源身份”的一部分。你改 URL,等于给资源换了个身份,全世界所有指向旧身份的东西都会失效。
这一章,我们把 URL 彻底拆开,搞清楚它到底是什么、为什么这么设计、以及你为什么不能随便改它。
6.1 先把问题问对:URI、URL、URN 到底什么关系
墨叔:先解决一个经典的糊涂账。URI、URL、URN 这三个词,面试常问,但 90% 的人说不清。
你:我大概知道 URL 是网址,URI 是更宽泛的标识,URN 是……名字?
墨叔:对,用一张图说清关系:
URI(统一资源标识符)
├── URL(统一资源定位符):通过"位置"来标识资源
│ http://example.com/a.jpg (告诉你去哪找)
└── URN(统一资源名称):通过"名字"来标识资源,与位置无关
urn:isbn:9787115428028 (告诉你它叫什么,不告诉你它在哪)
关键区别:
- URL 回答”它在哪、怎么拿到”——所以 URL 里带协议、带主机、带路径。
- URN 回答”它是什么”——只是一个稳定的名字,跟它在哪台服务器、什么路径无关。
- URI 是两者的总称。
你:那 URN 的意义是什么?我都没怎么见过。
墨叔:URN 想解决的是 URL 的一个致命缺陷——URL 绑定了”位置”,位置一变,标识就失效(就像小美改路径导致旧链接全崩)。
URN 的愿景是:给每个资源一个”永不改变的身份证号”,不管它搬到哪台服务器、哪个路径,这个名字永远有效。
但它失败了,原因很现实:
URN 需要一个”从名字到位置”的解析机制(类似 DNS 之于域名),而这个机制需要全世界的解析基础设施——这个基础设施从来没建起来。
所以今天的世界是:URL 赢了,URN 基本停留在纸面上。你只要知道”URI ⊇ URL”这个关系,以及”URN 是个没实现的理想”就够了。
6.2 URL 的解剖:五个部分,各管一件事
墨叔:把一个 URL 拆到底:
https://user:[email protected]:8443/v1/orders?page=2&size=20#summary
└─┬──┘ └───┬───┘ └────┬─────────┘└─┘└───────┬──────────┘└──┬──┘
scheme userinfo host port path query fragment
| 部分 | 作用 | 例子 |
|---|---|---|
| scheme | 用什么协议/机制去拿 | https、http、mailto、ws、file |
| userinfo | 认证信息(极少用,且危险) | user:pass@ |
| host | 哪台机器 | api.example.com |
| port | 哪个端口 | 8443 |
| path | 机器上的哪个”资源” | /v1/orders |
| query | 对这个资源的”参数化” | ?page=2&size=20 |
| fragment | 资源内部的”锚点” | #summary |
你:看起来挺清楚。但我知道它肯定有坑。
墨叔:坑多着呢。我们挑最关键的几个,尤其是那些”你以为懂,其实会翻车”的。
6.3 fragment 为什么服务器收不到?
墨叔:第一个坑,也是最能揭示 URL 设计哲学的:fragment(# 后面的部分)根本不会发给服务器。
GET /v1/orders?page=2&size=20 HTTP/1.1 <- 你看,请求行里没有 #summary
Host: api.example.com你:为什么?我明明写在 URL 里了。
墨叔:因为 fragment 的语义是”资源内部的一个位置”,而不是”另一个资源”。
它的设计意图是:一个 URL 指向一个完整的资源(比如一整个 HTML 文档),fragment 指向这个文档内部的某个小节(比如”第 3 章”)。
- 客户端请求的是整个资源(整个文档)。
- 拿到之后,客户端自己在本地定位到 fragment 对应的位置(浏览器滚动到
#summary那里)。
你:所以 fragment 是”客户端侧的”东西?
墨叔:对。它解决的是”一个资源太大,我想指向其中的一部分,但这一部分的划分是’内容层’的,服务器不需要知道”。
这个设计非常精妙——它把”定位”这件事分层了:
URL 的 path + query 负责”找到哪个资源”(服务器关心),fragment 负责”找到资源的哪个部分”(只有客户端关心)。
你:那现代 SPA(单页应用)里,#/user/123 这种把”路由”塞进 fragment 的做法,是不是就”滥用”了 fragment?
墨叔:好眼力。SPA 早期用 hash 路由(#/user/123),因为——改 fragment 不会触发页面刷新,也不会发请求,正好适合前端自己管路由。但这是”借用”了 fragment 的”不发给服务器”这个特性,把本属于”资源定位”的路由信息藏进了**本属于”资源内定位”的 fragment 里。
代价是什么?你想想。
你:……搜索引擎看不到 fragment,所以这些”页面”对 SEO 不友好?
墨叔:对。而且服务器永远不知道用户在看 /user/123 还是 /user/456,因为 fragment 根本没发过去。所以后来 SPA 纷纷改用了 History API(/user/123 变成真正的 path),代价是服务器要做”fallback 路由”(把所有路径都重定向回 index.html)。
这个故事很有启发:fragment 的设计是自洽的,但它的”不发给服务器”特性,让它成为一个诱人的”私货通道”——大家总想往里面塞本该发给服务器的东西,结果都要为这个”聪明”付出代价。
6.4 百分号编码:为什么 URL 里不能有空格
墨叔:下一个坑。为什么 URL 里出现空格、中文、& 这些字符时,要变成 %20、%E4%B8%AD、%26 这种形式?
你:因为 URL 有”保留字符”,这些字符有特殊含义?
墨叔:对。URL 里有一套保留字符,它们在语法里有特殊作用:
: / ? # [ ] @ —— 分隔符,有语法作用
! $ & ' ( ) * + , ; = —— 子分隔符
如果你的数据里恰好含有这些字符(比如用户名是 A&B,或者搜索关键词里有 ?),你就必须转义,否则解析器会把它当成语法分隔符,URL 就乱了。
转义方式就是百分号编码:% + 两个十六进制数,表示一个字节。比如空格(ASCII 32)= %20,&(ASCII 38)= %26。
你:那中文呢?中文不是 ASCII。
墨叔:这正是历史上最拧巴的地方。百分号编码编码的是字节,而一个中文字符在不同编码下是不同的字节:
- UTF-8 下,“中”是
E4 B8 AD三个字节 →%E4%B8%AD。 - GBK 下,“中”是
D6 D0两个字节 →%D6%D0。
URL 标准本身没规定”原始字符用什么字符集”(标准只规定”URL 是 ASCII 的,非 ASCII 先编码成字节”)。于是历史上客户端用 UTF-8,服务器用 GBK,就出现了中文乱码——这是 2000 年代中文网站最经典的 bug 之一。
现在的共识是:统一用 UTF-8。但遗留系统的坑还在,你接手老项目时,URL 中文乱码八成是这个原因。
你:所以”百分号编码”本质是把”任意字节”塞进”ASCII-only 的 URL”里的一种方式。
墨叔:精确。记住这个本质,你就能理解为什么它又叫”URL 编码”(URL encoding)了。
6.5 path vs query:定位 vs 参数化
墨叔:下一个高频的设计决策:什么放 path,什么放 query?
GET /v1/orders/123 <- path 里的 123
GET /v1/orders?id=123 <- query 里的 123你:这俩有啥区别?不都能拿到订单 123 吗?
墨叔:语义区别很大,而且影响深远:
| path | query | |
|---|---|---|
| 语义 | 资源的身份(这是哪个资源) | 资源的参数化(怎么筛选/分页/排序这个资源) |
| 例子 | /orders/123 = “订单 123 这个资源” | ?status=paid = “订单集合里,已支付的那些” |
| 对缓存 | 每个 path 是独立缓存键 | query 变了,通常也是新的缓存键 |
判断标准:问自己”这是在标识一个特定的东西,还是在筛选一堆东西?”
- “标识特定东西” → path(
/orders/123)。 - “筛选一堆东西” → query(
/orders?status=paid&page=2)。
你:那 ?id=123 这种算什么?
墨叔:它其实是在”筛选”——语义上是”从所有订单里,筛选出 id=123 的那一个”。能用,但不够精确:它把”身份”降格成了”筛选条件”,会丢失一些东西(比如语义上它不像一个”资源”了)。
更深一层:这个区分决定了你的资源模型是否清晰。REST 的思想(第 6 章后面会展开,其实我在第 1 章埋过伏笔)核心就是”一切皆资源,资源有身份”。path 就是”身份”,query 是”视图”。
一个实际的坑:如果你把分页参数放 path(/orders/page/2),那”第 2 页”就变成了一个”资源身份”,而不是”订单集合的一个视图”。这会导致缓存、路由都变别扭。分页、排序、筛选,永远放 query。
6.6 URL 长度限制:谁规定的?其实没人规定
墨叔:一个流传很广的”伪知识”:“GET 请求 URL 最长 2048 个字符”。
你:难道不是吗?我一直这么记的。
墨叔:协议从来没有规定 URL 的长度上限。
你听到的”2048""8192""2KB”这些数字,全是具体实现自己定的:
- IE 时代的老限制:2083 字符。
- 老 Nginx:默认 8KB。
- 现代浏览器:普遍支持几十 KB。
你:那为什么大家都说 GET 有长度限制?
墨叔:因为 GET 的参数在 URL 里,而 URL 又出现在请求行里,请求行有一个”所有实现都有限制”的隐性约束。而且URL 太长的真实代价是:
- 会被中间设备(代理、WAF、日志系统)截断或拒绝。
- 会泄露数据——URL 会出现在访问日志、浏览器历史、referer、分享链接里。
- 会污染缓存。
所以结论:
“GET 有长度限制”不是协议规定,是”现实世界对长 URL 不友好”的民间总结。 正确的做法不是”担心 URL 多长会爆”,而是”URL 里的数据本该就短——长的、敏感的数据放 body”。
6.7 路径规范化与安全:/../ 的陷阱
墨叔:下一个坑,安全相关的,而且坑过无数人。
URL 路径可以被”规范化”。比如 /a/b/../c 可以化简成 /a/c(.. 表示上一级)。问题在于:服务器做路径解析时,是先规范化还是先鉴权?顺序错了,就出漏洞。
经典攻击:路径穿越(Path Traversal)。
想象一个文件下载接口:
GET /download?file=../../etc/passwd
如果服务器天真地把 file 参数直接拼到文件路径里:
// 危险!没做规范化/校验
data, _ := os.ReadFile("/var/www/files/" + r.URL.Query().Get("file"))
// 用户传 ../../etc/passwd,就读到了系统的密码文件你:所以要先规范化、再校验、再拼接。
墨叔:对,而且顺序极其重要。正确的防御是:
- 先规范化路径(把
..、.、重复斜杠都解析掉)。 - 再校验规范化后的路径是否还在允许的目录内(
/var/www/files/前缀检查)。 - 最后才拼接到真实文件系统。
这个”顺序”就是安全漏洞的核心——任何”先信任、后校验”的代码,都等于没有校验。 规范化、解码、校验的先后顺序,是 URL 安全的第一课。
你:还有一个坑,百分号编码的”双重解码”?
墨叔:对,进阶版。攻击者可以发 %2e%2e%2f(这是 ../ 的编码形式)。如果服务器先做安全校验(没解码),再做解码去拼路径,那 %2e%2e%2f 在校验时是”无害的字符串”,解码后却变成了 ../——绕过校验。这就是”解码和校验的顺序”问题,和上面是同一个坑的不同变体。
6.8 设计者视角:到底什么是”资源”?
墨叔:这一章的最后,讲一个真正”深刻”的问题,它会把 URL、REST、以及第 15 章的”不变量”都串起来:
到底什么是”资源”?
你:资源就是……服务器上的东西?文件、数据、记录?
墨叔:这是最常见的误解,也是 REST 被”CRUD 化”误解的根源。让我引 Roy Fielding(REST 的提出者)的原意:
资源(resource)是”任何可以被命名、被标识的东西”——它可以是文档、图片、一个时间段的服务、一个概念、甚至”今天北京的天气”。
注意关键词:资源 ≠ 文件,资源 ≠ 数据库记录,资源是”一个可以被 URI 标识的抽象概念”。
你:抽象概念?那”今天北京的天气”算资源?
墨叔:算。你可以给它一个 URI:https://api.weather.com/beijing/today。这个 URI 指向的不是某个文件,而是”今天北京天气”这个概念,每次请求,服务器返回的是这个概念当前的表示(representation)。
这就是 REST 最关键、也最被误解的一层:
资源(resource)和表示(representation)是两回事。 资源是抽象的、可能随时变的(“北京天气”每时每刻在变);表示是具体的、某个时刻的快照(“2026-08-30 06:00 的天气是 26°C”)。
URL 标识的是”资源”,不是”表示”。 同一个 URL,你今天访问和明天访问,拿到的是不同的表示——因为资源变了,但资源本身还是”北京今天的天气”这个身份。
你:……我好像第一次真正理解 REST 在说什么。
墨叔:这就对了。所以业界把 REST 简化成”GET/POST/PUT/DELETE 对应增删改查”,其实丢掉了它最精华的部分——即”把世界建模成资源,用 URI 命名它们,用表示来传递状态”。
这引出一个对你有直接价值的结论:
设计接口时,第一个要想清楚的问题不是”用什么方法”,而是”我这里的’资源’到底是什么?”
如果你的接口是”动作导向”的(/doSomething、/getUserById),你还没想清楚资源是什么;如果你的接口是”资源导向”的(/users/123),你才开始了 REST 式的思考。
6.9 Go 落地:url 包里的三个坑
墨叔:Go 里解析 URL,有三个坑必须知道:
u, _ := url.Parse("https://example.com/search?q=hello%20world&lang=zh")
// 坑一:Query() 会帮你解码,但重复的 key 只取第一个
fmt.Println(u.Query().Get("q")) // "hello world"(已解码)
fmt.Println(u.RawQuery) // "q=hello%20world&lang=zh"(原始未解码)
// 坑二:Path 和 RawPath 的区别
// Path 是解码后的,RawPath 是原始的。编码过的路径要用 EscapedPath()
fmt.Println(u.Path) // 解码后的路径
fmt.Println(u.EscapedPath()) // 编码后的路径(转发时要用这个!)
// 坑三:直接拼字符串容易错,应该用 net/url 构造
v := url.Values{}
v.Set("q", "hello world")
v.Set("lang", "zh")
fmt.Println(v.Encode()) // "lang=zh&q=hello+world"(注意空格变成了 +)你:第三个,空格变成 + 了?这跟 %20 什么关系?
墨叔:问得好,这是个经典坑。在 query 里,空格可以编码成 %20,也可以编码成 +(这是历史遗留的两种约定,application/x-www-form-urlencoded 用 + 表示空格)。所以:
- 在 path 里,空格必须是
%20。 - 在 query 里,空格可能是
%20或+(看编码方式)。
Go 的 url.Values.Encode() 默认用 +,而你如果手动解码时只处理 %20 不处理 +,就会把 + 当成真的加号。这个 %20 vs + 的差异,坑过无数人。
6.10 排查清单:URL 相关问题的定位
第 1 步:URL 的哪个部分出了错?
→ scheme / host / port / path / query / fragment,先定位
第 2 步:是不是编码问题?
→ 中文乱码 = 字符集不一致(UTF-8 vs GBK)
→ 空格变 + = query 编码方式混用
第 3 步:是不是路径规范化/解码顺序问题?
→ 路径穿越、双重解码
第 4 步:改 URL 之前,评估了"身份变更"的后果吗?
→ SEO、收藏夹、缓存、第三方回调
第 5 步:URL 里的敏感数据泄露了吗?
→ 访问日志、浏览器历史、referer
思考题
第 1 题(概念)
请说明 URI、URL、URN 三者的关系,并回答:为什么”URN 的愿景”(给资源一个永不改变的、与位置无关的名字)在现实中基本失败了?
第 2 题(概念)
fragment(# 后面的部分)不会发送给服务器。请回答:
- 这个设计是为了解决什么问题?
- 现代 SPA 的 hash 路由利用了 fragment 的什么特性?
- 利用这个特性付出了什么代价?
第 3 题(设计)
你要设计一个”商品列表”接口,支持:按分类筛选、按价格排序、分页、按关键词搜索。
请设计这个接口的 URL 结构,并说明:
- 哪些信息放 path,哪些放 query,为什么?
- 如果要支持”收藏某个商品”,URL 应该长什么样?(提示:思考”资源”是什么)
第 4 题(生活类比)
墨叔:一个公司的工牌,和 URL,有什么相似之处?
提示:工牌上有”公司名(哪家公司)”+“部门(哪个部门)”+“工号(哪个人)“。而 URL 有 “host(哪台机器)” + “path(哪个资源)“。想想:
- 员工换部门了,工牌上的”部门”变了,但”工号”没变——这对应 URL 的什么问题?
- 一个员工跳槽到另一家公司,工号可能不变,但”公司名+部门”全变了——这又对应什么?
第 5 题(更深一层的发问)
“资源”和”表示”的区分(6.8 节)是 REST 的核心。现在把这个区分推向极致:
- “今天的天气”是一个资源,它每时每刻在变。那么,一个永远不变的资源存在吗?(比如”π 的值”)
- 如果资源永远不变,那”资源”和”表示”还有区别吗?
- 这引出一个更深的问题:URL 标识的到底是”一个东西”,还是”一个持续变化的、但身份稳定的容器”? 这跟”接口版本管理”有什么关系?(提示:想一下
https://api.weather.com/beijing/today和https://api.weather.com/beijing/2026-08-30的区别)
参考答案
第 1 题答案
关系:URI 是总称,URL 和 URN 是它的两种。URL 通过”位置”标识资源(告诉你去哪拿),URN 通过”名字”标识资源(与位置无关,只告诉你它叫什么)。
URN 为什么失败:
URN 的本质是”名字和位置解耦”——资源可以随便搬家,名字不变。但解耦的代价是:你需要一个”名字 → 位置”的解析服务(类似 DNS:你给域名,它给你 IP)。
这个解析服务要成立,需要:
- 全球统一的、权威的解析基础设施(谁能决定”这个 URN 指向哪台机器”?谁有权威?)。
- 所有人愿意用这个基础设施(而不是各自用 URL)。
而这两点都没有发生。原因:
- 权威问题:URL 的权威很自然——“这个域名属于我,我域名下的路径我随意定义”。但 URN 的权威归属模糊(
urn:isbn:xxx归 ISBN 中心管?那urn:mycompany:xxx归谁管?)。 - 激励问题:URL “开箱即用”——你拿到一个 URL,直接用 HTTP 就能访问,不需要任何第三方解析。而 URN 多了一层”查表”,每多一层,就多一个失败点、多一个需要协调的环节。
结论:
URN 的失败告诉我们:一个”更优雅、更解耦”的设计,如果需要一个”尚未存在的全球协调机制”,它就不会赢。 而 URL 的”耦合”(位置即身份)恰恰是它赢的原因——它不需要任何协调,开箱即用。
这和第 1 章”协议的成功取决于允许多少人犯错还活下来”是同一个道理:简单、零协调、开箱即用,永远比”优雅但需要协调”更可能在现实世界胜出。
第 2 题答案
1. fragment 解决什么问题: 解决”资源内部定位”。一个资源(如一个长文档)可能很大,我想指向其中的某个小节。fragment 让 URL 能表达”这个资源里的这个位置”,而服务器不需要理解资源内部的结构(那是客户端的职责)。
2. SPA hash 路由利用了 fragment 的什么特性: 利用了”修改 fragment 不会发送新请求、不会触发页面刷新”这个特性。所以前端可以自由地改 fragment 来实现”路由跳转”,而不会导致整页重载。
3. 付出了什么代价:
- 服务器永远看不到路由:
/app#/user/123和/app#/user/456对服务器是同一个请求(都是/app)。服务器无法做基于路由的日志、权限、SEO。 - SEO 失效:搜索引擎的爬虫不解析 fragment,所以这些”页面”对搜索引擎不可见(或需要额外的”hash 转 path”方案)。
- 语义错位:fragment 本意是”资源内定位”,现在被用来承载”资源定位”(哪个页面),是用错了语义层——所以后来 SPA 都迁移到了 History API(真 path 路由),代价是服务器要加 fallback 路由。
升华:这题和 6.3 节呼应——fragment 的”不发给服务器”是个特性,但它太诱人了,诱人到大家总想往里塞”本该发给服务器”的东西,然后为这个”聪明”付出代价。 协议设计里,“诱人的捷径”往往是有隐形成本的。
第 3 题答案
1. 哪些放 path,哪些放 query:
# 商品列表(集合)本身是一个资源
GET /products
# 筛选、排序、分页、搜索 —— 都是对"集合"的参数化,放 query
GET /products?category=electronics&sort=price_desc&page=2&size=20&q=iphone
判断依据(6.5 节):
category、sort、page、q都是”如何筛选/排列这个集合”,是视图参数,不是资源身份,所以放 query。- 如果把它们放 path(如
/products/electronics/page/2),就把”视图”错当成了”资源身份”,会导致缓存、路由、语义都混乱。
2. 收藏某个商品:
关键在于”收藏”这件事,资源是什么。有两种建模:
方案 A:收藏是一个”子资源”。
PUT /products/123/favorite # 收藏商品 123
DELETE /products/123/favorite # 取消收藏
GET /products/123/favorite # 查看是否已收藏
这里把”收藏”建模成了”商品 123 的一个子资源”,用 PUT(设置状态)和 DELETE(移除状态)来操作——这正是第 3 章说的”把 toggle 拆成两个幂等操作”。
方案 B:收藏是一个独立的”收藏资源”。
POST /favorites # body: {"product_id": 123},创建一条收藏
DELETE /favorites/xxx # 删除某条收藏
这里把”收藏”本身建模成资源(它有自己的身份、创建时间等属性)。
两个方案都对,取决于”收藏”有没有自己的属性:
- 如果收藏只是个开关(收藏/取消),方案 A 更简洁。
- 如果收藏有属性(收藏时间、备注、分组),方案 B 更合适(它是”一等公民”)。
核心要点:设计 URL 的本质,是设计”资源模型”——想清楚这里有哪些资源、它们是什么关系(包含/并列/子资源)。 方法(GET/PUT/POST)是其次的,资源模型才是骨架。
第 4 题答案
1. “换部门,工号不变” 对应 URL 的什么问题:
对应**“位置变了,身份失效”**——这是 URL 的核心缺陷。
员工的”工号”是稳定的身份,但”部门”是位置。工牌上如果只有”公司+部门+工号”作为身份(而没有独立的工号体系),那么一换部门,工牌就作废了——别人手里存的”你的旧工牌信息”就失效了。
这跟 URL 的问题一模一样:http://a.com/dept1/zhangsan 里,zhangsan(工号)藏在 dept1(部门)下面。一旦 dept1 改成 dept2,这个 URL 就失效了,所有指向旧 URL 的地方(收藏夹、链接、缓存)全部作废。
这就是小美在引子里”改路径导致 SEO 全没、旧链接全 404”的根本原因:URL 把”身份”和”位置”焊死了,位置一动,身份就崩。
2. “跳槽,公司名+部门全变,工号可能不变” 对应什么:
对应**“同一个资源,可以有多个 URL 指向它”**(或者反过来,“一个 URL 的身份,完全取决于它的位置”)。
员工跳槽,他的”本质”(这个人)没变,但”公司名+部门”全变了。如果身份完全绑定位置,那”这个人”就变成了”另一个身份”。
这对应的 URL 问题是:一个资源搬了家(从 a.com 搬到 b.com),它的 URL 全变了,看起来像”新资源”,但它其实是同一个资源。这就是为什么有”重定向”(301/302,第 4 章)——它是”旧位置”到”新位置”的搬家声明,用来给”身份随位置变”这个缺陷打补丁。
升华:
工牌和 URL 的共同困境,是”身份”和”位置”的耦合。 位置是易变的,身份应该稳定。把身份绑死在位置上(URL、工牌),就会面临”一变位置就断身份”的持续痛苦。URN 想解耦但失败了;重定向是”解耦失败”后的补丁;而现代系统里,大家用”资源的业务 ID”(如订单号)在应用层重建了稳定身份,URL 只是”入口”——这是对”身份-位置耦合”的又一种绕行。
第 5 题答案
1. 永远不变的资源存在吗?
在严格意义上,几乎不存在。因为:
- 即使”π 的值”这种数学常量,它的”表示”也可能变——今天服务器返回 100 位小数,明天返回 200 位(精度变了)。
- 更常见的是:资源是”一个持续更新的东西”(天气、股价、用户信息、新闻)。
所以”资源会变”是常态,“资源不变”是特例。
2. 如果资源永远不变,“资源”和”表示”还有区别吗?
没有区别了——或者说,区别变得无关紧要。
因为”资源 vs 表示”这个区分之所以有意义,恰恰是因为资源在变:
- 资源在变 → 同一资源的”表示”会随时间不同 → 所以需要区分”抽象的、持续的资源”和”具体的、某时刻的表示”。
- 资源不变 → 任何时候拿到的表示都一样 → “资源”和”表示”塌缩成一个东西。
这反过来揭示了”资源/表示”区分的本质:
“资源”和”表示”的区分,是用来处理”变化”的。 它让”一个会变的东西”也能有一个稳定的身份(URL),而每次访问拿到的”当前快照”就是表示。
3. URL 标识的到底是”东西”还是”持续变化的容器”?
是”持续变化的、但身份稳定的容器”。
https://api.weather.com/beijing/today标识的是”北京今天”这个容器——它的内容(温度)时刻在变,但身份(“北京今天”)稳定。https://api.weather.com/beijing/2026-08-30标识的是”北京 2026-08-30 这一天”——这是一个固定的、不可变的快照(过了这天,它的内容就永远定了)。
这跟接口版本管理的深刻关系:
这是本题的高潮。它揭示了两种完全不同的接口版本策略:
.../beijing/today= “滚动的最新版”:URL 不变,内容随时变。客户端永远拿到最新的,但也永远不知道什么时候会拿到一个”变了的东西”。这对应”不做版本管理,直接改接口”——老客户端可能随时被”最新”坑到。.../beijing/2026-08-30= “不可变的快照版”:URL 绑定到某个固定时点,内容永不改变。客户端拿到的是稳定的、可预期的。这对应”URL 版本化”(/v1、/v2)——每个版本是一个”不可变的快照”。
结论:
接口版本管理的本质,就是把”会变的资源”冻结成”不可变的快照”。
/v1/orders和/v2/orders不是两个”不同的资源”,而是”订单接口”这个持续变化的容器的两个冻结快照。冻结了,老客户端才能依赖它;不冻结,老客户端永远活在”接口可能随时变”的恐惧里。
所以,当你设计接口时,问自己一句:“我要给调用方的是一个’会变的 today’,还是一个’冻结的 2026-08-30’?” 这个问题的答案,决定了你的版本策略。
一页纸小结
URI ⊇ URL/URN:URL 用位置标识,URN 用名字标识(但 URN 因缺少全球解析基础设施而失败)。
URL 七部分:scheme / userinfo / host / port / path / query / fragment。
fragment 不发给服务器:它是”资源内定位”,是客户端的事。SPA hash 路由借用了它,付出了 SEO 失效的代价。
百分号编码:把任意字节塞进 ASCII-only URL。中文乱码 = 字符集不一致(UTF-8 vs GBK)。
path vs query:path 是身份(这是哪个资源),query 是视图(怎么筛选)。分页/排序/筛选放 query。
URL 长度:协议没规定,是实现的限制 + “长 URL 不友好”的民间总结。
路径穿越:规范化/解码/校验的顺序,是 URL 安全的核心。
资源 vs 表示:资源是抽象的、会变的;表示是具体的快照。URL 标识资源,不是表示。这是 REST 最精华、最被误解的一层。
Go 坑:Path vs EscapedPath、空格在 query 里可能是 +。
下一章:URL 和报文都讲完了,现在轮到那个让你”本地飞快、线上拉胯”的问题——连接。我要问你一个能让你重新理解”性能”的问题:为什么你的接口,第一个请求总是特别慢,后面的就快了? 以及那个困扰了 HTTP 整整 20 年的恶魔——队头阻塞。