---
title: "UTF-8 是 Ken Thompson 在一张餐巾纸上画出来的"
description: "回顾 1992 年 Ken Thompson 在餐巾纸上设计出 UTF-8 的经过与字符编码混战史，解释 Java、JavaScript 因选择 UCS-2 而留下 emoji 长度为 2 的历史包袱。"
date: 2026-04-21T11:54:32Z
canonical: https://xiaobox.github.io/p/2026-04-21-utf-8-shi-ken-thompson-zai-yi-zhang-can-jin-zhi-shang-hua-chu-lai-de/
author: 小盒子
categories: ["行业与思考"]
license: CC BY-NC-SA 4.0
license_url: https://creativecommons.org/licenses/by-nc-sa/4.0/
---

# UTF-8 是 Ken Thompson 在一张餐巾纸上画出来的


我前几天在 Java 里随手写了个小工具，要算一段用户昵称的长度，用来判断有没有超出数据库字段限制。代码跑出来我愣了一下

一个「🚀」，`length()` 返回 2。

我盯着屏幕看了几秒钟。其实这个坑不是第一次见，写 Java 几年的人多少都踩过。但踩了还不长记性，每次遇到都得重新想一下。一个 emoji 是一个字符吧，肉眼看就是一个，怎么到了 Java 里就变成 2 了。

说到这个我又想起来，JavaScript 也是一样。`"🚀".length === 2`，你自己拿浏览器控制台试就知道。更早的坑是 Python 2，那时候一段中文写错编码 `UnicodeDecodeError` 能让你卡一整天。

这玩意不是 bug。是 1992 到 1996 那四年，几家大公司选错了编码方案，我们今天还在替他们填坑。

这事儿真正的源头，在 1992 年 9 月 2 日那个周三晚上，美国新泽西一家路边小餐馆的餐桌上。

餐桌旁坐着两个人，一个叫 Ken Thompson，写过 Unix；另一个叫 Rob Pike，写过 Plan 9。

Thompson 在一张餐巾纸上写了几行二进制。

那张纸被服务员收走扔了。

但纸上那几行二进制活下来了，今天全世界 98% 的网页、你手机里每一条聊天消息，都靠着那几行二进制的规矩在跑。

![](https://xiaobox-public-images.oss-cn-beijing.aliyuncs.com/images/ChatGPT%20Image%202026%E5%B9%B44%E6%9C%8820%E6%97%A5%2023_44_16.png)

---

要讲明白那一夜发生的事，得先讲一下它发生之前的 30 年，世界上的字符编码有多乱。

一切从 1963 年开始。那年美国标准协会发布了 ASA X3.4-1963，也就是后来的 ASCII。主导者叫 Bob Bemer，在 IBM 干过很多年，被人叫 ASCII 之父。

ASCII 是 7 位的，128 个字符。26 个英文字母大小写加上数字、标点、回车、空格，够英文世界用了。

第二年 1964，IBM 自己跳票。他们本来也是 ASCII 委员会的主力之一，但 System/360 大型机要发布，外设赶不及改成 ASCII，于是老大拍板，我们先用原来的 BCD 扩展一下，搞了个叫 EBCDIC 的 8 位编码顶上。这个「临时方案」一用就是 60 年，今天的银行大型机、航空公司订票系统还在跑 EBCDIC。不过这是另一个故事了。

到了 80 年代，事情开始乱起来。

1980 年我国出了 GB2312，7445 个汉字，够日常用。1982 年日本出了 Shift-JIS，是日本 ASCII 公司做的。1984 年台湾资策会出了 Big5，繁体汉字。1987 年欧洲出了 ISO 8859-1，也就是后来大家熟的 Latin-1，把西欧那些带重音的字母塞进去。

你数数，光是那几年，亚洲和欧洲就至少有四套互相不兼容的编码在并行跑。每个国家各自为政，各自的文本文件，到了别国的机器上一读就是满屏乱码。

互联网一起来，直接就炸了。

你想象一下，1989 年一个日本人给一个中国人发邮件，中间经过美国的服务器。日本人用 Shift-JIS 编码，中国人的机器按 GB2312 解码。结果就是，那个邮件在中国人屏幕上是一片乱码，没有任何办法能自动读出来。

这不是工程师不努力。这是一个结构性问题，每套编码都占着 0 到 255 这 256 个字节值，但对应的字符完全不同。一个字节值在 Shift-JIS 里是「あ」，在 GB2312 里可能是一个汉字的半截，在 Latin-1 里是个西班牙字母。机器看到一串字节，根本不知道应该用哪套表去翻译。

到了 1988 年，Xerox 和 Apple 的几个工程师坐下来说，这不行，得搞一个全世界通用的编码。他们拉了一堆公司组了个 Unicode 联盟。1991 年出了 Unicode 1.0。

Unicode 的想法很理想，给世界上所有文字一个唯一的编号，英语里的 A 是 65，中文的「中」是 20013。编号空间留得很大，理论上能装下所有文字。

但 Unicode 1.0 的编码方式选错了。

他们选了 UCS-2，16 位定长。每个字符不管是英文还是中文，都占 2 个字节。65536 个坑，当时看够用了，思路也简单，处理字符串就像处理数组。

这个选择有个根上的毛病。

C 语言里每个字符串都以 `\0` 也就是 0 字节作为结尾标志。ASCII 里英文字母 A 是 65，B 是 66，都不会出现 0。但到了 UCS-2，英文字母 A 变成了两个字节 `00 41`，高字节就是 0。C 程序一读这个字符串，看到第一个 0 就停了，认为字符串已经结束，后面全丢。

全世界几十年积累下来的 C 代码，一夜作废。

你是一个操作系统厂商，你敢不敢让用户升级一下，然后所有老程序都跑不起来？你不敢。

这时候就有人站出来了，国际组织 X/Open 委员会。他们想搞一个过渡方案，既能表达全世界的字符，又能兼容老的 C 程序。他们拉了几个人，出了一份草案，叫 FSS/UTF，全称 File System Safe UTF。

1992 年 9 月 2 日，周三下午，X/Open 的人正在德州奥斯汀开会讨论这份草案。其中有几个是 IBM 的，他们觉得这方案还得再听听懂行人怎么说。于是他们打了个电话到新泽西的贝尔实验室，找 Ken Thompson 和 Rob Pike 帮忙审一下。

电话接通了。

---

![](https://xiaobox-public-images.oss-cn-beijing.aliyuncs.com/images/20260420234611113.png)

Pike 后来在 2003 年的一封邮件里详细回忆了那天晚上发生了什么。

电话挂了之后他和 Thompson 对了一下。方案有个大问题，从一段字节流中间任何位置切进去，FSS/UTF 分不清自己现在读到的这个字节是字符起点，还是某个字符的中间某一段。这听起来是个小毛病，其实是大事，比如一个文本文件损坏了前半段，你拿后半段打开应该还能看，而 FSS/UTF 做不到。

Thompson 想了想，说我能重写一个。

他们那天晚上就去新泽西的一家 diner 吃饭，具体是哪一家 Pike 没说过，反正就是那种美国公路边上的老式餐馆。Thompson 拿了一张餐巾纸，在上面写 bit-packing 的方案。

他的方案核心只有一个想法，让每个字节自带位置信息。

ASCII 字符的 8 个 bit，首位是 0。也就是说所有老 C 程序里存的英文字母，在新方案里完全不变。一个字节 `0xxxxxxx`，就代表这是一个单独的 ASCII 字符。完全兼容。

多字节字符怎么办。Thompson 给每个多字节字符的首字节加一个长度前缀。

两个字节的字符，首字节是 `110xxxxx`，第二个字节是 `10xxxxxx`。一看首字节以 110 开头，就知道这是一个 2 字节字符的开头。

三个字节的字符，首字节是 `1110xxxx`，后面两个字节都是 `10xxxxxx`。

四个字节的字符，首字节是 `11110xxx`，后面三个字节都是 `10xxxxxx`。

所有非首字节都是 `10xxxxxx` 开头。

这想法太妙了。你从字节流任何一个位置切进去，看一眼当前字节的最高几位，就能判断自己是处在一个字符的起点，还是中间。如果是 `10` 开头，往回找，找到第一个不是 `10` 开头的字节就是字符的起点。整个字节流可以从任何地方切断重连，都能自动同步。  

Pike 说他当时在旁边看着，Ken 一笔一笔在餐巾纸上写这套规则。

吃完饭回到贝尔实验室，他俩打电话给 X/Open 的委员会，说我们有一个改进的方案，给你们。

当天晚上 Thompson 就开始写编码和解码的 C 代码。Pike 分头改 Plan 9 操作系统的 C 库和图形库。两个人通宵。

第二天周四，代码基本写完。周五，整个 Plan 9 操作系统已经跑在这个新编码上了。

从餐桌上那张纸到一个完整操作系统全量切换，一共 3 天。

9 月 8 日周二凌晨 3 点 22 分，Thompson 发了一封邮件出去，公告这事。邮件开头写的是 `Here is our modified FSS-UTF proposal.`，那会儿这玩意还叫「修改过的 FSS-UTF 方案」，UTF-8 这个名字是后来起的。

邮件最后一句 Thompson 说，`The code has been tested to some degree and should be pretty good shape.`，意思是代码测过了，应该差不多能用。

https://doc.cat-v.org/bell_labs/utf-8_history

![](https://xiaobox-public-images.oss-cn-beijing.aliyuncs.com/images/ChatGPT%20Image%202026%E5%B9%B44%E6%9C%8820%E6%97%A5%2023_55_16.png)

半年后，1993 年 1 月，USENIX 冬季大会在圣迭戈开。Thompson 和 Pike 在会上把这套方案作为 UTF-8 正式对外发布。

那张餐巾纸呢。

Pike 在 2003 年给 Markus Kuhn 的邮件里写过一句话，原话是 `I very clearly remember Ken writing on the placemat and wished we had kept it!` 我记得很清楚 Ken 在餐巾纸上写东西的那一幕，真希望我们当时把那张纸留下来了。

他等了 11 年才说出那句话。

---

故事到这里还没完。真正的剧情是，Thompson 公告过后，世界并没有立刻跟上。

1993 年 7 月 27 日，Thompson 公告 UTF-8 整整 10 个月之后，微软发布了 Windows NT 3.1。NT 系列第一个版本，内部字符串选了 UCS-2。10 个月听起来长，但做操作系统的人都懂，一个产品要发布，18 个月前整套架构就定死了，UTF-8 那会儿刚出来，Windows 这一版已经没时间改了。我能理解。

两年过去了。1995 年 5 月，Sun 发布 Java 1.0。`char` 类型 16 位，内部 UCS-2。这回没有「来不及」的理由，UTF-8 已经公告快 3 年，USENIX 大会上都讲过。Sun 的工程师是看过、想过、然后主动选的 UCS-2。

半年后。1995 年 12 月，网景发布 JavaScript，规范文档里直接写明 String 是 16 位整数序列。一点模糊空间都没留，直接在规范层面把 16 位写死了。这一条到今天的 ECMAScript 标准里都没改过。

这三家的理由是一样的。UCS-2 定长 16 位好处理，每个字符 2 字节，字符串操作就像操作数组，`charAt(i)` 直接拿第 i 个字符，简单粗暴。65536 个字符当年看起来绰绰有余。他们都没想到，一年之后这个假设就被打破了。

然后就到了 1996 年 7 月。

Unicode 联盟发布 2.0 版本。他们发现当年预留的 65536 个坑不够，要放进更多的汉字、日韩字、历史文字、后来还要放 emoji，远远超出 65536。于是他们引入了一个机制叫 surrogate pair，中文叫代理对。

Unicode 从这一版开始，突破了 16 位边界。对于超出 65536 的字符，编号需要拆成两个 16 位的「代理半对」来表示。原来的 UCS-2 从这一刻起，被迫升级为 UTF-16。

但这一升级，UCS-2 那个「定长、好处理」的核心优势，彻底没了。

一个 emoji 的 Unicode 编号超过 65536，到了 UTF-16 里就得用两个 16 位代理对来表示。Java 和 JavaScript 都是把字符串当成 16 位 code unit 的序列，一个 emoji 就占两个 code unit。

所以我开头说的那一幕，`"🚀".length()` 返回 2、`"🚀".length === 2`，就是从那一年留下来的病根。

![](https://xiaobox-public-images.oss-cn-beijing.aliyuncs.com/images/ChatGPT%20Image%202026%E5%B9%B44%E6%9C%8820%E6%97%A5%2023_49_58.png)

Java 后来在 Java 5 引入了对代理对的处理函数，但 `String.length()` 返回的还是 code unit 数，不是真正的字符数。改不了，因为改了就破坏所有老代码。Windows API 那套 `LPWSTR`、`wchar_t` 也一样，至今 Windows 内部字符串还是 UTF-16。

这些平台今天还的债，都是 1993 到 1995 那两年做错决定时欠的。

另一边是 Unix 阵营。Plan 9、Linux、后来的 macOS、Go、Rust、Python 3，默认字符串都是 UTF-8。这些平台当年都是跟着 Thompson 那张餐巾纸走的。

我国这一路走得更曲折。GB2312 不够用，1995 年扩成 GBK，21886 个汉字。2000 年再升级成 GB18030，成为强制标准。相当长一段时间里，国内的软件、网页、文档都是 GBK 和 UTF-8 并行。你现在打开一些 2005 年前后的老网页，还能看到 `charset=gb2312` 或者 `charset=gbk`。新项目基本全切到 UTF-8 了。

2012 年前后，W3C 的 HTML 规范把 UTF-8 定为 Web 的标准编码。今天 W3Techs 的统计显示，全球 98% 以上的网页都在跑 UTF-8。

---

过去那些年的那些事儿，我一直觉得挺有意思。

你看当年 X/Open 委员会在奥斯汀开会，一屋子专家，流程走完起草完一套方案，花了不知道多少人月。Unicode 联盟一屋子工程师、语言学家、标准化官员，开了几年会出了 UCS-2。

Thompson 和 Pike 两个人，一顿饭的工夫，重画了一套方案，直接推翻桌子上那份草案，6 天之内让一个完整操作系统跑在新方案上，30 年后这套方案是全世界的事实标准。

而且两个人都没想到会有今天这个结果。Pike 的原邮件里写得很清楚，他当时只是觉得 FSS/UTF 有个可以改进的技术问题，顺手改一下。他不知道这一改，改出了一套用 30 年还在用的东西。

技术世界里能决定未来的那些决定，很多时候不是在会议室里开出来的，是在餐桌上一张纸上画出来的。很多看着「就该是这样」的默认规则，背后都是某一天某个具体的人做的具体的选择。

也许他们只是在那一天，正好在那个位置上，做了一个当时看起来合理的选择。然后世界就按这个选择跑了几十年。

所以 Java 里 `"🚀".length()` 是 2。所以你打开老文件会看到乱码。所以你在国内写代码时会偶尔遇到编码陷阱。这些都不是天经地义。都是当年某个会议、某个决定、某个工程师的一念之差。

那张餐巾纸 Pike 很遗憾没留下。但其实想想，这纸没留下来也挺好。留下来了，它就只是一张纸，放在博物馆里。没留下来，它的设计就活在你屏幕上每一个字节里。

你下次在 Java 里遇到 `"🚀".length()` 返回 2 的时候，想想 1995 年那个 Sun 做决定的人。

你下次看到中文字符正常显示的时候，想想 1992 年 9 月 2 日那个周三晚上，Ken Thompson 在一张餐巾纸上写下的 `110xxxxx 10xxxxxx`。

这种事，计算机这个行业里还有很多。每一个你习以为常的默认值背后，都站着一个早就被忘掉的、做那个选择的人。

---
