<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Tony Bai &#187; 国际化</title>
	<atom:link href="http://tonybai.com/tag/%e5%9b%bd%e9%99%85%e5%8c%96/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Thu, 09 Apr 2026 00:20:15 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>当“安全性”遭遇“交付速度”：2026 年，我为什么告别了 Rust</title>
		<link>https://tonybai.com/2026/02/21/safety-vs-delivery-speed-why-farewell-rust-in-2026/</link>
		<comments>https://tonybai.com/2026/02/21/safety-vs-delivery-speed-why-farewell-rust-in-2026/#comments</comments>
		<pubDate>Fri, 20 Feb 2026 23:58:47 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[BoilerplateCode]]></category>
		<category><![CDATA[BorrowChecker]]></category>
		<category><![CDATA[BusinessLogic]]></category>
		<category><![CDATA[CI/CD]]></category>
		<category><![CDATA[CompilationTime]]></category>
		<category><![CDATA[DeliverySpeed]]></category>
		<category><![CDATA[DynamicBusiness]]></category>
		<category><![CDATA[EcosystemMaturity]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GoLanguage]]></category>
		<category><![CDATA[Go语言]]></category>
		<category><![CDATA[i18n]]></category>
		<category><![CDATA[IterationVelocity]]></category>
		<category><![CDATA[MemorySafety]]></category>
		<category><![CDATA[MentalBurden]]></category>
		<category><![CDATA[nodejs]]></category>
		<category><![CDATA[OpenSourceEcosystem]]></category>
		<category><![CDATA[ResourceConsumption]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[safety]]></category>
		<category><![CDATA[SoftwareEngineering]]></category>
		<category><![CDATA[StaticConstraints]]></category>
		<category><![CDATA[TechSelection]]></category>
		<category><![CDATA[TypeSystem]]></category>
		<category><![CDATA[WebDevelopment]]></category>
		<category><![CDATA[Web开发]]></category>
		<category><![CDATA[交付速度]]></category>
		<category><![CDATA[借用检查器]]></category>
		<category><![CDATA[内存安全]]></category>
		<category><![CDATA[动态业务]]></category>
		<category><![CDATA[商业逻辑]]></category>
		<category><![CDATA[国际化]]></category>
		<category><![CDATA[安全性]]></category>
		<category><![CDATA[开源生态]]></category>
		<category><![CDATA[心智负担]]></category>
		<category><![CDATA[技术选型]]></category>
		<category><![CDATA[样板代码]]></category>
		<category><![CDATA[生态成熟度]]></category>
		<category><![CDATA[类型系统]]></category>
		<category><![CDATA[编译时间]]></category>
		<category><![CDATA[资源占用]]></category>
		<category><![CDATA[软件工程]]></category>
		<category><![CDATA[迭代速度]]></category>
		<category><![CDATA[静态约束]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5921</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2026/02/21/safety-vs-delivery-speed-why-farewell-rust-in-2026 大家好，我是Tony Bai。 在软件工程的铁三角中，Rust 占据了“安全性”与“性能”的绝对高地。凭借借用检查器（Borrow Checker）和极其严格的类型系统，它向开发者承诺了一个没有内存错误、没有空指针崩溃的完美世界。 然而，在商业软件开发的战场上，还有一个至关重要的维度往往被技术纯粹主义者忽视，那就是——交付速度（Delivery Speed）。 近日，资深工程师 Dmitry Kudryavtsev 发表了长文《Farewell, Rust》，详述了他为何忍痛将一个运行了多年、已盈利的 Rust 项目全盘重写为 Node.js 的心路历程。这篇文章也引发了一场关于“为了极致的安全性，我们是否值得牺牲过多的交付速度？”的深刻辩论。 缘起：一个 C/C++ 老兵的“安全梦” Dmitry 绝非那些被即时编译（JIT）宠坏的脚本小子。相反，他的技术底色是硬核的 C/C++。 早在高中时代，他就沉迷于指针的魔力，痴迷于手动管理内存的掌控感。他写过 3D 渲染器、IRC 机器人，甚至操作系统内核。然而，由于第一份工作是 PHP Web 开发，他被迫进入了动态语言的世界。虽然 PHP、Python 和 Ruby 带来了 Web 开发的极速体验，但在内心深处，他始终怀念 C 语言那种“压榨硬件每一滴性能”的快感，同时也痛恨 C 语言中防不胜防的内存安全漏洞。 直到 Rust 横空出世。 对于像 Dmitry 这样的工程师来说，Rust 简直就是“鱼与熊掌兼得”的梦想： 低级控制力：像 C 一样精确控制内存布局。 安全性：编译器在编译阶段就能消除一整类内存错误。 现代体验：拥有像 Cargo [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2026/safety-vs-delivery-speed-why-farewell-rust-in-2026-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2026/02/21/safety-vs-delivery-speed-why-farewell-rust-in-2026">本文永久链接</a> &#8211; https://tonybai.com/2026/02/21/safety-vs-delivery-speed-why-farewell-rust-in-2026</p>
<p>大家好，我是Tony Bai。</p>
<p>在软件工程的铁三角中，Rust 占据了“安全性”与“性能”的绝对高地。凭借借用检查器（Borrow Checker）和极其严格的类型系统，它向开发者承诺了一个没有内存错误、没有空指针崩溃的完美世界。</p>
<p>然而，在商业软件开发的战场上，还有一个至关重要的维度往往被技术纯粹主义者忽视，那就是——<strong>交付速度（Delivery Speed）</strong>。</p>
<p>近日，资深工程师 Dmitry Kudryavtsev 发表了长文《<a href="https://yieldcode.blog/post/farewell-rust/">Farewell, Rust</a>》，详述了他为何忍痛将一个运行了多年、已盈利的 Rust 项目全盘重写为 Node.js 的心路历程。这篇文章也引发了一场关于“为了极致的安全性，我们是否值得牺牲过多的交付速度？”的深刻辩论。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/go-network-programming-complete-guide-pr.png" alt="" /></p>
<h2>缘起：一个 C/C++ 老兵的“安全梦”</h2>
<p>Dmitry 绝非那些被即时编译（JIT）宠坏的脚本小子。相反，他的技术底色是硬核的 C/C++。</p>
<p>早在高中时代，他就沉迷于指针的魔力，痴迷于手动管理内存的掌控感。他写过 3D 渲染器、IRC 机器人，甚至操作系统内核。然而，由于第一份工作是 PHP Web 开发，他被迫进入了动态语言的世界。虽然 PHP、Python 和 Ruby 带来了 Web 开发的极速体验，但在内心深处，他始终怀念 C 语言那种“压榨硬件每一滴性能”的快感，同时也痛恨 C 语言中防不胜防的内存安全漏洞。</p>
<p>直到 Rust 横空出世。</p>
<p>对于像 Dmitry 这样的工程师来说，Rust 简直就是“鱼与熊掌兼得”的梦想：</p>
<ul>
<li>低级控制力：像 C 一样精确控制内存布局。</li>
<li>安全性：编译器在编译阶段就能消除一整类内存错误。</li>
<li>现代体验：拥有像 Cargo 这样优秀的包管理工具。</li>
</ul>
<p>于是，他做了一个所有热血工程师都会做的决定：为了追求极致的质量与安全，用 Rust 从零构建一个商业 Web 应用。</p>
<p>起初，一切都很完美。他在 2023 年底成功上线了项目，甚至因此受邀在两个技术大会上发表演讲。但随着时间的推移，业务逻辑日益复杂，“安全性”的红利开始被“交付速度”的损耗所抵消。到了 2026 年初，为了项目的生存，他不得不做出了那个艰难的决定：<strong>告别 Rust</strong>。</p>
<h2>深度复盘：Rust 在 Web 交付中的“五大减速带”</h2>
<p>Dmitry 的文章之所以珍贵，是因为他用亲身经历证明了：在 Web 开发的特定场景下，Rust 引以为傲的“安全性”机制，如何一步步变成了拖慢“交付速度”的罪魁祸首。</p>
<h3>1. 模板与视图：类型安全 vs. 迭代速度</h3>
<p>在后端逻辑中，Rust 的类型系统坚不可摧。但当数据流向前端（HTML/Email 模板）时，这种为了安全而设计的严格性，变成了修改 UI 时的噩梦。</p>
<ul>
<li>安全性的代价：为了保证编译时的类型安全，Rust 社区诞生了 Maud 或 Askama 这样的编译时模板库。它们通过宏（Macro）在编译期检查 HTML 模板中的每一个变量引用。这听起来很棒，意味着你永远不会渲染出错误的变量。</li>
<li>速度的牺牲：但这带来的副作用是，每次修改 HTML 哪怕一个标点符号，都会触发漫长的重新编译。在 Web 前端开发这种需要“所见即所得”的高频迭代场景下，这种等待是毁灭性的。</li>
<li>对比 Node.js：TypeScript 配合 JSX/TSX 提供了全链路的类型安全，同时保持了极快的热重载（Hot Reload）速度。重构一个字段，VS Code 会立即标红所有受影响的视图组件，修改后毫秒级生效。这种“安全且快”的体验，是 Rust 目前无法提供的。</li>
</ul>
<h3>2. 国际化（i18n）：生态缺失带来的效率黑洞</h3>
<p>对于商业应用，支持多语言是刚需。</p>
<p>虽然 Mozilla 开发了 Project Fluent，但 Rust 生态中缺乏成熟的、开箱即用的 i18n 解决方案。你往往需要为了“正确性”而去处理繁琐的加载逻辑和类型绑定，编写大量的胶水代码。而Node.js生态中的i18next 等库不仅极其成熟，还能配合 TypeScript 提供键值级别的类型安全。Node.js 原生内置了完整的 ICU 标准（Intl API），处理货币、日期、复数格式化信手拈来。在这一点上，Rust 开发者需要花费数倍的时间来实现同样的功能，严重拖慢了产品推向全球市场的速度。</p>
<h3>3. “动态”业务 vs. “静态”约束</h3>
<p>Web 业务充满了动态性：用户提交的 JSON 结构可能是不确定的，筛选条件的组合可能是无穷的。Rust 试图用静态类型系统去约束这些动态行为，结果就是开发效率的暴跌。</p>
<ul>
<li>序列化之痛：serde 是 Rust 的瑰宝，但在处理复杂的、充满 Option<T> 的业务数据时，为了安全地取出一个嵌套字段，你不得不编写大量的 match 或 unwrap 处理代码。为了优雅地处理错误，Dmitry 定义了十几个自定义错误枚举。虽然代码很健壮，但写起来太慢了。</li>
<li>SQL 的僵局：sqlx 提供了极其强大的编译时 SQL 检查，这在静态查询时非常棒。但是，一旦你需要根据用户输入动态构建查询（例如：用户选了 A 筛选条件就加个 WHERE 子句），Rust 的强类型系统就变成了噩梦。你无法像在 Node.js 中使用 Kysely 或 Prisma 那样，流畅地拼接查询片段。为了“安全”地构建 SQL，你付出了巨大的代码复杂度成本。</li>
</ul>
<h3>4. 编译时间：CI/CD 的隐形杀手</h3>
<p>这是最让 Dmitry 崩溃的一点，也是“交付速度”最直观的体现。</p>
<ul>
<li>Rust 的等待：随着依赖增多（尤其是使用了大量宏的 Web 框架），编译时间呈指数级增长。Dmitry 的 CI 流程需要 12-14 分钟 才能完成部署。“每次我在 Sentry 上看到一个简单的 Bug，想到修复它需要等待 15 分钟的构建流程，我就失去了修复的动力。”</li>
<li>Node.js 的极速：迁移到Node.js后，完整的 CI 流程（含 Lint 和测试）仅需 5 分钟。部署速度提升了 3 倍。这意味着“发现 Bug -> 修复 -> 上线”的反馈闭环被大大缩短了。在商业竞争中，修复速度往往比绝对的“无 Bug”更重要。</li>
</ul>
<h3>5. 生态成熟度：造轮子的时间成本</h3>
<p>Rust 的 Web 生态虽然在成长，但面对长尾需求时仍显稚嫩。</p>
<ul>
<li>场景：你需要集成一个冷门的第三方支付网关，或者处理一个特定的 Webhook 签名验证。</li>
<li>Rust 的困境：官方 SDK？没有。社区库？两年前就不更新了。为了安全，你不得不对着 API 文档，自己手写 HTTP 请求、自己实现加密验签逻辑。这占用了大量本该用于开发业务核心功能的时间。</li>
<li>Node.js 的便利：npm install 通常能解决一切。几乎所有 SaaS 服务商都会提供第一方的 Node.js SDK。“拿来主义”是提升交付速度的最佳捷径。</li>
</ul>
<h2>总结与反思：我们到底为了什么而编程？</h2>
<p>Dmitry 的文章并没有否定 Rust 的价值。相反，他依然热爱 Rust，依然怀念那些与编译器“斗智斗勇”并最终获得完美代码的日子。</p>
<p>他的结论非常客观，为所有正在做技术选型的团队提供了一把衡量“安全”与“速度”的标尺：</p>
<ol>
<li>
<p>资源占用 vs. 开发效率的账本<br />
Rust 版本的应用内存占用仅 60-80MB，而 Node.js 版本约为 117MB。<br />
Rust 确实更省资源。但对于业务应用来说，这 <strong>50MB</strong> 的内存差异，在云服务器几美元一个月的成本面前不值一提。然而，为了节省这 50MB 内存，开发者付出了几倍的开发时间、调试精力以及心智负担。这笔账，在商业逻辑上是划不来的。</p>
</li>
<li>
<p>技术选型的“黄金法则”</p>
<ul>
<li>何时拥抱“安全性”（选 Rust）：如果你在构建数据库内核、搜索引擎、高频交易系统、嵌入式设备固件，或者像 Lambda 这样对冷启动时间极度敏感的 Serverless 函数。在这些场景下，性能和稳定性是核心竞争力，为了安全牺牲开发速度是值得的。</li>
<li>何时拥抱“交付速度”（选 Node.js/Go/Python）：如果你在构建 CRUD 后端、SaaS 业务逻辑、内部管理工具，或者处于需要快速试错、频繁变更需求的初创阶段。在这些场景下，迭代速度（Velocity）才是核心竞争力。</li>
</ul>
</li>
<li>
<p>给 Go 开发者的启示<br />
有趣的是，Dmitry 在注脚中提到了 Go：“Yes, there is Go. But I never really had the chance to like Go.”<br />
这其实是一个非常有意思的信号。在 Rust 的“极致安全”和 Node.js 的“极致速度”之间，<strong>Go 恰恰占据了那个“黄金平衡点”</strong>：</p>
<ul>
<li>它有静态编译和类型系统，比 Node.js 更安全、性能更好。</li>
<li>它有极快的编译速度和简单的语法，比 Rust 的心智负担低得多。</li>
<li>它有极其成熟的中间件和微服务生态。</li>
</ul>
<p>对于那些厌倦了 Node.js 运行时错误，又被 Rust 借用检查器拖慢脚步的 Web 开发者来说，Go 依然是当下最务实的选择。</p>
</li>
</ol>
<h2>小结</h2>
<p>技术选型从来没有绝对的优劣，只有“最适合当下约束条件的工具”。</p>
<p>Dmitry 的故事提醒我们：不要因为手里拿着“安全性”这把锤子（Rust），就无视了“交付速度”这个钉子。在商业软件的世界里，有时候，为了让产品活下去，为了让用户更快用上新功能，“足够好”且“跑得快”的代码，往往比“完美但迟到”的代码更有价值。</p>
<p>Rust 是系统编程的未来，但这并不意味着它是所有 Web 业务的终点。对于独立开发者或初创团队而言，“快”，本身就是一种极其重要的功能。</p>
<p>资料链接：https://yieldcode.blog/post/farewell-rust/</p>
<hr />
<p><strong>你会为了“安全”放弃“速度”吗？</strong></p>
<p>软件工程永远是权衡的艺术。在你的项目中，你是否也曾为了追求某种“先进特性”，而导致项目进度失控？如果给你 50MB 的内存节省，你愿意多等 10 分钟的编译时间吗？</p>
<p>欢迎在评论区分享你的选型纠结！</p>
<hr />
<p>还在为“复制粘贴喂AI”而烦恼？我的新专栏 <strong>《<a href="http://gk.link/a/12EPd">AI原生开发工作流实战</a>》</strong> 将带你：</p>
<ul>
<li>告别低效，重塑开发范式</li>
<li>驾驭AI Agent(Claude Code)，实现工作流自动化</li>
<li>从“AI使用者”进化为规范驱动开发的“工作流指挥家”</li>
</ul>
<p>扫描下方二维码，开启你的AI原生开发之旅。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/ai-native-dev-workflow-qr.png" alt="" /></p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2026, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2026/02/21/safety-vs-delivery-speed-why-farewell-rust-in-2026/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>收到非 UTF-8 文本怎么办？Go 字符集检测的探索与实践</title>
		<link>https://tonybai.com/2025/10/17/detect-charset-in-go/</link>
		<comments>https://tonybai.com/2025/10/17/detect-charset-in-go/#comments</comments>
		<pubDate>Thu, 16 Oct 2025 23:26:49 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[C/C++]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[Collation]]></category>
		<category><![CDATA[ConfidenceScore]]></category>
		<category><![CDATA[Conversion]]></category>
		<category><![CDATA[Detection]]></category>
		<category><![CDATA[EUC-JP]]></category>
		<category><![CDATA[Formatting]]></category>
		<category><![CDATA[GB18030]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[golang.org/x/text]]></category>
		<category><![CDATA[Go字符集检测]]></category>
		<category><![CDATA[Go生态]]></category>
		<category><![CDATA[i18n]]></category>
		<category><![CDATA[ICU]]></category>
		<category><![CDATA[ICU4C]]></category>
		<category><![CDATA[InternationalComponentsforUnicode]]></category>
		<category><![CDATA[Java库]]></category>
		<category><![CDATA[latin-1]]></category>
		<category><![CDATA[locale]]></category>
		<category><![CDATA[NGram频率分析]]></category>
		<category><![CDATA[saintfish/chardet]]></category>
		<category><![CDATA[Shift-JIS]]></category>
		<category><![CDATA[String]]></category>
		<category><![CDATA[TextBoundaries]]></category>
		<category><![CDATA[Transformation]]></category>
		<category><![CDATA[trigram]]></category>
		<category><![CDATA[uber-go/icu4go]]></category>
		<category><![CDATA[Unicode联盟]]></category>
		<category><![CDATA[utf-8]]></category>
		<category><![CDATA[windows-1252]]></category>
		<category><![CDATA[单字节编码]]></category>
		<category><![CDATA[启发式算法]]></category>
		<category><![CDATA[国际化]]></category>
		<category><![CDATA[地域]]></category>
		<category><![CDATA[多字节编码]]></category>
		<category><![CDATA[字符集检测]]></category>
		<category><![CDATA[字符集检测工具]]></category>
		<category><![CDATA[字节流]]></category>
		<category><![CDATA[归档]]></category>
		<category><![CDATA[指纹匹配]]></category>
		<category><![CDATA[文本比较]]></category>
		<category><![CDATA[文本转换]]></category>
		<category><![CDATA[文本边界]]></category>
		<category><![CDATA[未知编码]]></category>
		<category><![CDATA[格式化]]></category>
		<category><![CDATA[概率性猜测]]></category>
		<category><![CDATA[状态机解码器]]></category>
		<category><![CDATA[纯Go]]></category>
		<category><![CDATA[统计学]]></category>
		<category><![CDATA[统计学分析]]></category>
		<category><![CDATA[置信度]]></category>
		<category><![CDATA[转换]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5263</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/10/17/detect-charset-in-go 大家好，我是Tony Bai。 在上一篇关于 Go 语言 string 与 rune 设计哲学的文章发布后，我收到了许多精彩的反馈。其中，一位读者提出了一个极具现实意义的后续问题：“既然 Go 的世界以 UTF-8 为中心，那么当我们从外部系统（如老旧的文件、非标准的 API）接收到一段未知编码的字节流时，我们该如何是好？Go 生态是否有成熟的字符集检测工具/库？” 这个问题，将我们从 Go 语言舒适、有序的“理想国”，直接拉回了那个充满了历史遗留问题、编码标准五花八门的“现实世界”。 字符集检测，本质上是一种“隐式”的、带有猜测成分的“黑魔法”。本文将和大家一起探讨这门“黑魔法”背后的原理，审视 Go 生态中现有的解决方案，并最终回答那个核心问题：在 Go 中，我们应该如何优雅地处理未知编码的文本。 在我们深入探讨具体的 Go 库及其实现之前，建立一个正确的预期至关重要。我们必须首先理解这门“黑魔法”的本质，明白为何字符集检测是一项与编码转换截然不同、且充满不确定性的任务。 字符集检测——一门“不精确”的科学 在我们深入探讨具体的 Go 库及其实现之前，我们必须建立一个核心认知：字符集检测与编码转换截然不同，其本质上不是一个确定性的过程，而是一个基于启发式算法和统计学的概率性猜测。 它就像一位语言学家，仅凭一小段文字（字节序列），就要猜出这段文字是用哪国语言（编码）写成的。 如果文本足够长且特征明显，他可能会充满信心地说：“这看起来 99% 是日语 Shift-JIS。” 如果文本很短，或者内容模棱两可，他可能只能给出一个模糊的答案：“这可能是 latin-1，也可能是 windows-1252。” 在最坏的情况下，他甚至可能完全猜错。 因此，任何字符集检测工具，其返回的结果都应该被理解为一个带有置信度 (Confidence Score) 的“最佳猜测”，而非一个 100% 准确的真理。 既然我们已经认识到字符集检测是一门“不精确”的科学，那么我们的探索自然会引向一个问题：在整个软件行业中，谁是解决这个难题的权威？我们继续往下探索。 行业黄金标准——ICU 是什么？ 在字符集检测乃至整个国际化（i18n）领域，ICU (International Components for [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/detect-charset-in-go-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/10/17/detect-charset-in-go">本文永久链接</a> &#8211; https://tonybai.com/2025/10/17/detect-charset-in-go</p>
<p>大家好，我是Tony Bai。</p>
<p>在上一篇关于 Go 语言 <a href="https://tonybai.com/2025/10/13/string-and-rune-in-go">string 与 rune 设计哲学</a>的文章发布后，我收到了许多精彩的反馈。其中，一位读者提出了一个极具现实意义的后续问题：“既然 Go 的世界以 UTF-8 为中心，那么当我们从外部系统（如老旧的文件、非标准的 API）接收到一段<strong>未知编码</strong>的字节流时，我们该如何是好？Go 生态是否有成熟的字符集检测工具/库？”</p>
<p>这个问题，将我们从 Go 语言舒适、有序的“理想国”，直接拉回了那个充满了历史遗留问题、编码标准五花八门的“现实世界”。</p>
<p>字符集检测，本质上是一种“隐式”的、带有猜测成分的“黑魔法”。本文将和大家一起探讨这门“黑魔法”背后的原理，审视 Go 生态中现有的解决方案，并最终回答那个核心问题：在 Go 中，我们应该如何优雅地处理未知编码的文本。</p>
<p>在我们深入探讨具体的 Go 库及其实现之前，建立一个正确的预期至关重要。我们必须首先理解这门“黑魔法”的本质，明白为何字符集检测是一项与编码转换截然不同、且充满不确定性的任务。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/go-network-programming-complete-guide-pr.png" alt="" /></p>
<h2>字符集检测——一门“不精确”的科学</h2>
<p>在我们深入探讨具体的 Go 库及其实现之前，我们必须建立一个核心认知：字符集检测与编码转换截然不同，其<strong>本质上不是一个确定性的过程，而是一个基于启发式算法和统计学的概率性猜测。</strong></p>
<p>它就像一位语言学家，仅凭一小段文字（字节序列），就要猜出这段文字是用哪国语言（编码）写成的。</p>
<ul>
<li><strong>如果文本足够长且特征明显</strong>，他可能会充满信心地说：“这看起来 99% 是日语 Shift-JIS。”</li>
<li><strong>如果文本很短，或者内容模棱两可</strong>，他可能只能给出一个模糊的答案：“这可能是 latin-1，也可能是 windows-1252。”</li>
<li><strong>在最坏的情况下</strong>，他甚至可能完全猜错。</li>
</ul>
<p>因此，任何字符集检测工具，其返回的结果都应该被理解为一个带有<strong>置信度 (Confidence Score)</strong> 的“最佳猜测”，而非一个 100% 准确的真理。</p>
<p>既然我们已经认识到字符集检测是一门“不精确”的科学，那么我们的探索自然会引向一个问题：在整个软件行业中，谁是解决这个难题的权威？我们继续往下探索。</p>
<h2>行业黄金标准——ICU 是什么？</h2>
<p>在字符集检测乃至整个国际化（i18n）领域，<a href="https://icu.unicode.org/">ICU (International Components for Unicode)</a> 是绕不开的“黄金标准”。</p>
<ul>
<li><strong>它是什么？</strong> ICU 是一套由 Unicode 联盟维护的、极其成熟和全面的 C/C++ 和 Java 库。它为应用程序提供了强大的 Unicode 和全球化支持，是无数大型软件（从操作系统到浏览器）背后处理文本的“隐形英雄”。</li>
<li><strong>它能做什么？</strong> ICU 的能力远不止字符集检测，它是一个庞大的工具集，为处理全球化文本提供了“全家桶”式的解决方案，包括：
<ul>
<li><strong>文本比较 (Collation)</strong>：提供符合特定语言文化习惯的字符串排序规则。
<ul>
<li><strong>示例</strong>：在德语中，”Österreich”（奥地利）应该排在 “Zürich”（苏黎世）之前，即使 Ö 在 Unicode 码点上可能大于 Z。在瑞典语中，å, ä, ö 被视为独立的字母，排在 z 之后。ICU 的 Collation 服务能正确处理这些复杂的排序逻辑。</li>
</ul>
</li>
<li><strong>格式化 (Formatting)</strong>：精确地格式化和解析日期、时间、数字、货币，并能处理不同地域的表示习惯。
<ul>
<li><strong>示例</strong>：数字 12345.67 在美国被格式化为 “12,345.67&#8243;，但在德国则会是 “12.345,67&#8243;。同样，日期 2025年9月26日 在美国可能是 “September 26, 2025&#8243;，在法国则是 “26 septembre 2025&#8243;。ICU 能根据指定的地域 (Locale) 自动进行正确的格式化。</li>
</ul>
</li>
<li><strong>文本转换 (Transformation)</strong>：支持大小写转换、全半角转换、音译等。
<ul>
<li><strong>示例</strong>：将土耳其语中的 i 转换为大写，正确的结果应该是带点的 İ，而不是 I。ICU 知道这个特殊的转换规则。它还可以将俄语中的西里尔字母 “Москва” 音译为拉丁字母 “Moskva”。</li>
</ul>
</li>
<li><strong>文本边界 (Text Boundaries)</strong>：能根据不同语言的规则，准确的识别出字符边界、字边界、换行边界以及句子边界。</li>
</ul>
</li>
<li><strong>它的重要性？</strong> ICU 是处理国际化文本领域权威且全面的解决方案。它的算法和数据经过了数十年的积累和验证，是业界公认的“事实标准”。</li>
</ul>
<p>了解了 ICU 在行业中的泰斗地位后，我们自然会好奇其强大能力的来源。现在，就让我们揭开这层神秘的面纱，深入探究其字符集检测算法，究竟是如何在一堆无序的字节中，扮演“文本侦探”的角色的。</p>
<h2>ICU 的检测算法——“指纹”与“统计”的侦探艺术</h2>
<p>ICU 的字符集检测算法是业界公认最强大的之一，其“侦探工作”主要分为两大策略，分别应对不同类型的编码。</p>
<h3>策略一：多字节编码的“指纹匹配”</h3>
<p>对于像 UTF-8, GBK, Shift-JIS 这样的多字节编码，它们的字节序列都具有明确的“语法规则”或“指纹”。检测器为每种多字节编码都实现了一个<strong>状态机解码器</strong>。</p>
<p>多字节编码字符集的检测流程如下图(参考saintfish/chardet的实现整理)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/detect-charset-in-go-2.png" alt="" /></p>
<p><strong>核心流程说明</strong>：</p>
<ol>
<li><strong>逐字符解码</strong>：解码器尝试从字节流中一次解码一个字符。例如，一个 Shift-JIS 解码器知道，如果遇到一个 0&#215;81-0x9F 或 0xE0-0xFC 范围内的字节，那么它后面必须跟一个 0&#215;40-0xFE 范围的字节，两者才能组成一个合法的双字节字符。</li>
<li><strong>统计与评分</strong>：在解码过程中，算法会统计几个关键指标：
<ul>
<li><strong>双字节字符数</strong> (doubleByteCharCount)</li>
<li><strong>错误字节数</strong> (badCharCount)</li>
<li><strong>常用字符命中数</strong> (commonCharCount)：每个编码器都内置了一张包含 50-100 个高频字符的“指纹”列表。解码出的每个字符都会在这张表里进行快速二分查找。</li>
</ul>
</li>
<li><strong>计算置信度</strong>：
<ul>
<li><strong>提前退出</strong>：如果错误率过高（例如，badCharCount * 5 >= doubleByteCharCount），则该编码器会立即放弃，返回置信度 0。</li>
<li><strong>综合评分</strong>：如果没有提前退出，则会根据上述指标进行综合评分。匹配到的常用字符越多，置信度越高。为了防止长文本导致过度自信，算法还采用了<strong>对数缩放</strong>来计算最终得分。</li>
</ul>
</li>
</ol>
<p>这种基于“语法规则”和“高频词指纹”的匹配方式，使得多字节编码的识别相对精确。</p>
<h3>策略二：单字节编码的“统计学分析”</h3>
<p>对于像 latin-1 或 windows-1252 这样的单字节编码，几乎任何字节序列都是“合法”的，“指纹匹配”策略在此失效。此时，检测器会切换到<strong>统计学分析</strong>模式。下面是单字节编码字符集的检测流程示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/detect-charset-in-go-3.png" alt="" /></p>
<p><strong>核心流程说明</strong>：</p>
<ol>
<li><strong>字符规范化</strong>：首先，通过一个预定义的 charMap 表，将输入的字节流进行规范化处理，例如将大写字母转为小写，将重音符号转为基础字母，将多种标点符号统一视为空格。</li>
<li><strong>N-gram 频率分析</strong>：算法在一个 3 字节的滑动窗口（即 trigram）中分析文本。每个语言的识别器都内置了一张包含 64 个最常见 trigram 的频率表（例如，英语的频率表会包含  a ,  an,  be 等序列）。</li>
<li><strong>计算命中率与置信度</strong>：通过二分查找，计算输入文本中的 trigram 在预定义频率表中出现的次数（ngramHit）。
<ul>
<li><strong>高置信度</strong>：如果命中率超过一个阈值（如 33%），则认为匹配度很高，直接给出一个接近满分（如 98）的置信度。</li>
<li><strong>按比例评分</strong>：如果命中率较低，则按比例将其缩放到 0-100 的范围内。</li>
</ul>
</li>
</ol>
<p>通过检测器会并发地运行所有这些识别器，最终将结果按置信度从高到低排序，返回最佳的猜测。</p>
<h2>CGO 方案的启示——uber-go/icu4go 的能力与局限</h2>
<p>在了解了 ICU 的字符集检测算法后，我们终于可以进入实践环节。将 ICU 的强大能力引入 Go 生态，最直接的路径是什么？答案似乎是构建一座通往其原生 C 库（ICU4C）的桥梁。</p>
<p>Go 社区曾有过这样的尝试，其中最著名的就是 Uber 开源的 uber-go/icu4go。这是一个通过 <strong>CGO</strong>，为 ICU4C 提供 Go 语言封装的库。然而，当我们深入探究这个库时，却发现了一个意想不到的事实。</p>
<p>尽管底层的 ICU4C 库确实拥有强大的字符集检测功能（定义于 ucsdet.h），但 uber-go/icu4go 这个 Go 封装<strong>并没有暴露这部分 API</strong>。它主要专注于 ICU 的另一部分核心能力：</p>
<ul>
<li><strong>本地化 (Locale)</strong>：处理不同地域的语言和文化习惯。</li>
<li><strong>格式化 (Formatting)</strong>：提供对数字、货币、日期和时间的精确本地化格式化。</li>
</ul>
<p>这意味着，即使我们愿意承担引入 CGO 的所有代价，uber-go/icu4go 也<strong>无法直接解决我们的字符集检测问题</strong>。</p>
<blockquote>
<p>注：uber-go/icu4go 如今已stable且被<strong>归档 (Archived)</strong>。</p>
</blockquote>
<p>不过，对于追求简洁的 Go 社区来说，为了一个功能而引入额外沉重的 C 依赖，往往被认为是得不偿失的。这次对 CGO 方案的探索虽然未能直接解决我们的问题，但它清晰地指明了方向：要寻找一个真正符合 Go 语言哲学的解决方案，我们必须将目光投向“纯 Go 之路”。</p>
<h2>纯 Go 方案——saintfish/chardet 的移植与局限</h2>
<p>用纯 Go 来实现字符集检测是否可行？答案是肯定的。saintfish/chardet 就是这样一个库，它是 ICU 字符集检测算法的一个纯 Go 语言移植版本。</p>
<p>下面是使用chardet对utf-8、GB-18030和eu-jp字符集进行检测的示例：</p>
<pre><code class="go">// https://go.dev/play/p/pxjc_XxDF8v
package main

import (
    "fmt"

    "github.com/saintfish/chardet"
)

func main() {
    // 示例: 检测字节数组的字符集
    detectFromBytes()
}

// detectFromBytes 检测字节数组的字符集
func detectFromBytes() {
    // 不同编码的示例文本
    texts := map[string][]byte{
        "UTF-8 中文": []byte("这是一段UTF-8编码的中文文本"),
        "GB18030 中文": []byte{
            // "Go是Google开发的一种静态强类型、编译型语言"的GB18030编码
            71, 111, 202, 199, 71, 111, 111, 103, 108, 101, 233, 95, 176, 108, 181, 196, 210, 187, 214, 214, 190, 142, 215, 103, 208, 205, 163, 172, 129, 75, 176, 108, 208, 205, 163, 172, 178, 162, 190, 223, 211, 208, 192, 172, 187, 248, 187, 216, 202, 213, 185, 166, 196, 220, 181, 196, 177, 224, 179, 204, 211, 239, 209, 212,
        },
        "日文 EUC-JP": []byte{
            // "こんにちは世界" 的EUC-JP编码示例
            164, 179, 164, 243, 164, 203, 164, 193, 164, 207, 192, 164, 179, 166,
        },
    }

    // 创建文本检测器
    detector := chardet.NewTextDetector()

    for name, data := range texts {
        fmt.Printf("\n=== 检测: %s ===\n", name)

        // 方法1: 获取最佳匹配
        result, err := detector.DetectBest(data)
        if err != nil {
            fmt.Printf("检测失败: %v\n", err)
            continue
        }
        fmt.Printf("最佳匹配:\n")
        fmt.Printf("  字符集: %s\n", result.Charset)
        fmt.Printf("  语言: %s\n", result.Language)
        fmt.Printf("  置信度: %d%%\n", result.Confidence)

        // 方法2: 获取所有可能的匹配
        results, err := detector.DetectAll(data)
        if err != nil {
            fmt.Printf("检测所有匹配失败: %v\n", err)
            continue
        }
        fmt.Printf("\n所有可能的匹配:\n")
        for i, r := range results {
            fmt.Printf("  %d. %s (语言: %s, 置信度: %d%%)\n",
                i+1, r.Charset, r.Language, r.Confidence)
        }
    }
}
</code></pre>
<p><strong>这个示例的输出如下:</strong></p>
<pre><code>$go run main.go

=== 检测: 日文 EUC-JP ===
最佳匹配:
  字符集: GB-18030
  语言: zh
  置信度: 10%

所有可能的匹配:
  1. Shift_JIS (语言: ja, 置信度: 10%)
  2. GB-18030 (语言: zh, 置信度: 10%)
  3. EUC-JP (语言: ja, 置信度: 10%)
  4. EUC-KR (语言: ko, 置信度: 10%)
  5. Big5 (语言: zh, 置信度: 10%)

=== 检测: UTF-8 中文 ===
最佳匹配:
  字符集: UTF-8
  语言:
  置信度: 100%

所有可能的匹配:
  1. UTF-8 (语言: , 置信度: 100%)
  2. windows-1253 (语言: el, 置信度: 20%)
  3. Big5 (语言: zh, 置信度: 10%)
  4. Shift_JIS (语言: ja, 置信度: 10%)
  5. GB-18030 (语言: zh, 置信度: 10%)

=== 检测: GB18030 中文 ===
最佳匹配:
  字符集: GB-18030
  语言: zh
  置信度: 100%

所有可能的匹配:
  1. GB-18030 (语言: zh, 置信度: 100%)
  2. Big5 (语言: zh, 置信度: 10%)
  3. Shift_JIS (语言: ja, 置信度: 10%)
  4. windows-1252 (语言: fr, 置信度: 5%)
</code></pre>
<p>这个结果生动地印证了我们在本文开头的论断：字符集检测是一门“不精确”的科学。对于短小的日文 EUC-JP 文本(14个字节)，chardet 发生了误判(将之识别为GB-18030)，给出了一个置信度仅为 10% 的错误答案。</p>
<p>根据之前我们对检测算法的了解，这次日文检测失败的主要原因很可能是数据量太少。我们提供给检测器的日文 EUC-JP 数据只有 14 字节，这对于字符集检测来说太短了，导致所有候选编码的置信度都只有 10%。下面我们提供更多日文字符，看看检测器是否能做出正确的检测！</p>
<p>这次我们提供的日文字符如下：</p>
<pre><code>"日文 EUC-JP": []byte{
            // "Go言語はGoogleが開発したプログラミング言語です。並行処理が得意で、コンパイル速度も速いです。日本語のテストです。"
            71, 111, 184, 192, 184, 236, 164, 207, 71, 111, 111, 103, 108, 101, 164, 172, 179, 171, 200, 175, 164, 183, 164,
            191, 165, 215, 165, 237, 165, 176, 165, 233, 165, 223, 165, 243, 165, 176, 184, 192, 184, 236, 164, 199, 164, 185,
            161, 163, 202, 195, 185, 212, 189, 232, 164, 234, 164, 172, 196, 192, 176, 213, 164, 199, 161, 162, 165, 179, 165,
            243, 165, 209, 165, 164, 165, 235, 194, 174, 197, 249, 164, 226, 194, 174, 164, 164, 164, 199, 164, 185, 161, 163,
            198, 252, 203, 220, 184, 236, 164, 206, 165, 198, 165, 185, 165, 200, 164, 199, 164, 185, 161, 163,
        },
</code></pre>
<p>然后再运行一次检测器，这次得到的结果如下：</p>
<pre><code>// 忽略其他
=== 检测: 日文 EUC-JP ===
最佳匹配:
  字符集: EUC-JP
  语言: ja
  置信度: 100%

所有可能的匹配:
  1. EUC-JP (语言: ja, 置信度: 100%)
  2. GB-18030 (语言: zh, 置信度: 59%)
  3. Big5 (语言: zh, 置信度: 48%)
  4. ISO-8859-1 (语言: fr, 置信度: 11%)
  5. ISO-8859-6 (语言: ar, 置信度: 10%)
  6. Shift_JIS (语言: ja, 置信度: 10%)
  7. EUC-KR (语言: ko, 置信度: 10%)
  8. ISO-8859-7 (语言: el, 置信度: 9%)
  9. windows-1256 (语言: ar, 置信度: 8%)
  10. KOI8-R (语言: ru, 置信度: 6%)
  11. ISO-8859-9 (语言: tr, 置信度: 3%)
</code></pre>
<p>这回检测器做出了正确的检查！</p>
<p>在日常做字符集检测时，有一个<strong>置信度阈值建议</strong>：</p>
<ul>
<li><strong>>= 80%</strong>: 可以较高把握地采纳该结果。</li>
<li><strong>50-80%</strong>: 结果可疑，建议结合其他业务逻辑进行验证，或提示用户进行人工确认。</li>
<li><strong>&lt; 50%</strong>: 结果几乎不可信，应视为检测失败。</li>
</ul>
<p>尽管 chardet 能够工作，但它也面临其自身的<strong>局限</strong>：早已<strong>不再积极维护</strong>。这意味着它可能缺少对新编码的支持，也可能存在未修复的 Bug。</p>
<h2>标准库的边界——golang.org/x/text 能做什么？</h2>
<p>看到 icu4go 和 chardet 两个关键库都已不再活跃，一个自然的问题是：<strong>我们能否仅依靠 Go 官方的 golang.org/x/text下面的包，自己实现一个字符集检测工具呢？</strong></p>
<p>最初我也想当然的认为这是可行的。但经过调查后，才发现答案：<strong>几乎不可能。</strong> x/text/encoding 包的设计目标是<strong>转换 (Conversion)</strong>，而非<strong>检测 (Detection)</strong>。</p>
<p>它提供了一套极其强大和高效的工具，用于在<strong>已知源编码和目标编码</strong>的情况下，进行精确的转换。它就像一个多语言的“翻译官”，但前提是你必须告诉他：“请把这段 GBK 编码的文本，翻译成 UTF-8。”</p>
<pre><code class="go">import (
    "golang.org/x/text/encoding/simplifiedchinese"
    "golang.org/x/text/transform"
    "io"
    "os"
)

func convertGBKtoUTF8(gbkReader io.Reader) io.Reader {
    // gbkReader 是一个读取 GBK 编码文件的 io.Reader
    // utf8Reader 将会是一个在读取时自动转换为 UTF-8 的 io.Reader
    utf8Reader := transform.NewReader(gbkReader, simplifiedchinese.GBK.NewDecoder())
    return utf8Reader
}
</code></pre>
<p>由此也可以看出，Go标准库(包括golang.org/x/&#8230;)为你提供了最强大、最正确的转换工具，但将“猜测”这个不确定的、充满风险的任务，留给了开发者自己或第三方库去解决。它不提供用于“猜测”的统计模型或状态机。</p>
<h2>小结</h2>
<p>在梳理完所有线索后，我们终于可以为“Go 开发者如何处理未知编码”这个问题，给出一份清晰的实践指南：</p>
<ol>
<li>
<p><strong>最高法则：尽可能避免检测</strong>。在设计系统时，应始终将<strong>显式声明编码</strong>作为第一原则。例如：</p>
<ul>
<li><strong>HTTP API</strong>：强制要求客户端在 Content-Type 头中明确指定 charset。</li>
<li><strong>文件上传</strong>：在 UI 中提供一个下拉菜单，让用户（如果可能）指定其上传文件的编码。</li>
<li><strong>系统间通信</strong>：在服务间约定统一使用 UTF-8。</li>
</ul>
</li>
<li>
<p><strong>务实的选择：当必须检测时</strong>。如果你的业务场景（如处理用户上传的各种历史遗留文件）让你别无选择，那么：</p>
<ul>
<li>saintfish/chardet 是目前最符合 Go 语言习惯（纯 Go、无 CGO）的<strong>起点</strong>。尽管它已不再活跃，但其代码和原理依然是构建自定义解决方案的最佳参考。</li>
<li>在使用任何检测库时，<strong>必须对返回的置信度进行判断</strong>，并为低置信度的结果设计 fallback 逻辑。</li>
<li>可以考虑自己维护一个 chardet 的 fork，或者参考其原理，针对你的特定业务场景（例如，只在几种有限的编码中进行猜测）实现一个更轻量级的检测器。</li>
</ul>
</li>
<li>
<p><strong>最后的手段：CGO 的重量级武器</strong>。如果你的应用场景对检测的准确率要求极高，且你愿意承担 CGO 带来的所有复杂性，那么封装 ICU4C 依然是一条可行的、但充满挑战的道路。</p>
</li>
</ol>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p><strong>想系统学习Go，构建扎实的知识体系？</strong></p>
<p>我的新书《<a href="https://book.douban.com/subject/37499496/">Go语言第一课</a>》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-primer-published-4.png" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/10/17/detect-charset-in-go/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>也谈C语言对国际化的支持</title>
		<link>https://tonybai.com/2011/07/01/also-talk-about-the-internationalization-support-in-c/</link>
		<comments>https://tonybai.com/2011/07/01/also-talk-about-the-internationalization-support-in-c/#comments</comments>
		<pubDate>Fri, 01 Jul 2011 14:51:00 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[C99]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[Internationalization]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[国际化]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[程序员]]></category>

		<guid isPermaLink="false">http://tonybai.com/2011/07/01/%e4%b9%9f%e8%b0%88c%e8%af%ad%e8%a8%80%e5%af%b9%e5%9b%bd%e9%99%85%e5%8c%96%e7%9a%84%e6%94%af%e6%8c%81/</guid>
		<description><![CDATA[<p>C语言对国际化的支持由来已久，最初开始于其第一版标准，即C89标准。在C89中我们可以看到用于支持国际化的locale.h、宽字符、宽字符串以及多字节字符(串)。而之后的"C89增补1"标准，即C90标准，以及C95标准又进一步完善了C语言对国际化的支持，增加了wchar...</p>]]></description>
			<content:encoded><![CDATA[<p>C语言对国际化的支持由来已久，最初开始于其第一版标准，即C89标准。在C89中我们可以看到用于支持国际化的locale.h、宽字符、宽字符串以及多字节字符(串)。而之后的&quot;C89增补1&quot;标准，即C90标准，以及C95标准又进一步完善了C语言对国际化的支持，增加了wchar.h、 wctype.h以及大量用于操作宽字符(串)和多字节字符(串)的标准库函数。最新一版C语言标准，即<a href="http://tonybai.com/2005/07/28/introduction-on-c-standard-overview-series/" target="_blank">C99</a>，让C语言对国际化的支持变得更加成熟，对非英语字符集也给予了更好的支持。</p>
<p>C语言支持国际化的核心就是大家所熟知的locale技术。C语言中的locale模型于C90标准中被引入。locale模型使得一些库函数的外部行为依赖于locale设置。这样的好处就是你无需重新编译代码，你发布的应用即可根据locale来满足不同区域人们的文化习惯。locale包含若干个类别，诸如LC_CTYPE、LC_COLLATE等，其中每个类别都会独立影响某些C函数的外部行为。比较常见的诸如日期时间显示方式、货币表示方式等。</p>
<p>例如，LC_TIME影响strftime的外部行为，不同locale情况下strftime输出的结果会有不同，见下面示例：</p>
<p>int main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; time_t now;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; char buf[1024];</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; setlocale(LC_ALL, &quot;&quot;); /* set locale to current locale, which is &quot;zh_CN.GB18030&quot; */</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; time(&#038;now);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; strftime(buf, sizeof(buf), &quot;%a, %d %b %Y %H:%M:%S GMT&quot;, localtime(&#038;now));<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;%s\n&quot;, buf);</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; setlocale( LC_TIME, &quot;en_US.UTF-8&quot; );<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; memset(buf, 0, sizeof(buf));<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; strftime(buf, sizeof(buf), &quot;%a, %d %b %Y %H:%M:%S GMT&quot;, localtime(&#038;now));<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;%s\n&quot;, buf);<br />
	}</p>
<p>这个程序在我的RedHat上输出的结果如下：<br />
	五, 01&nbsp; 7月 2011 10:07:59 GMT<br />
	Fri, 01 Jul 2011 10:07:59 GMT</p>
<p>locale另外一个重要的作用就是对<a href="http://tonybai.com/2007/11/03/also-talk-about-char-encoding/" target="_blank">字符集转换</a>的影响。曾几何时，ASCII字符集曾是计算机上通行的字符集标准，那时的程序员一般根本无需考虑字符集转换。ASCII的好处就是每个字符可以存储在一个字节(8bit)中，其内部表示(Internel Representation)和外部表示(External Representation)是一致的，这样一来，其存储和传输都非常方便。程序内部在内存中对ASCII字符（就是一个字节）的处理（识别字符、计算字符串中字符个数、解析字符串等）也十分简单快捷。不过随着国际化的日益深入，ASCII的缺点便暴露了出来，即其编码集太小了，即便将8个bit都算上，最多也就是256(2的8次方)个字符，这丝毫没有考虑到广大亚洲人民的需要，严重&quot;伤害&quot;了亚洲人民的情感^_^。于是乎亚洲各个国家和地区都纷纷&quot;自己动手，丰衣足食&quot;，制定了适合自己国家民族语言文字的字符集标准（当然了，其他大洲的国家也是这个样子的）。这些新字符集编码在满足本国语言需要的同时，也都兼容ASCII字符集，也就是说都是在ASCII字符集的基础上通过扩展字节个数达到支持更多字符的目的的。由于兼容ASCII，所以这些字符集中字符的表示都是非固定长度的，即在ASCII编码区间内的字符(即ASCII字符)用一个字节表示；超出这个区间，就会用2个或3个或更多的字节表示。这样的字符在C语言中被归类称为&quot;多字节字符(multi-bytes character)&quot;。</p>
<p>多字节字符，有着与ASCII同样的优点，即它们是面向字节的，便于传输和存储。之前用于处理ASCII的字符设备（基于字节的）都可以对多字节字符给予很好的支持。不过多字节字符缺点也同样明显。</p>
<p>首先就是程序内部（在内存中）处理起来十分不便。给定一个存储了某种字符集字符的字节数组，如果你没有对应的解析器，你是无法识别字符边界，无法识别出数组中究竟包含了哪些字符的，更不用说返回字符个数等操作了。针对这一问题，C语言引入宽字符的概念，宽字符集中的字符所占用的字节数是相同的，要么都是2 个字节，要么都是4个字节（3字节不利于计算机内存寻址优化），一般最大就是4个字节了，因为4个字节已经可以涵盖全球已知所有语言的所有字符了。在 GCC中，默认C语言宽字符类型，即wchar_t类型的长度为4。我们在内存中操作宽字符显然要比多字节字符更加容易：每个字符与N字节一一对应，这样对于统计字符个数、解析和识别字符大有裨益。因此在考量了多字节字符和宽字符的特点后，一般我们会使用宽字符作为字符在程序中的内部表示（用在各种内存操作中），而在存储、传输和显示过程中则使用多字节字符。再多罗嗦几句：宽字符为何不适于传输和存储呢？大致有以下三个原因：</p>
<p>- 空间利用率不高，或者说比较浪费空间和带宽<br />
	我想这个原因不用过多解释了。如果用4字节的宽字符存储一篇英文文章，那么与多字节字符相比，宽字符要浪费3/4的空间。</p>
<p>- 字节序问题<br />
	宽字符一般用2或4个字节表示，这样的字符在存储和传输过程中显然会遇到字节序问题，不同的平台采用不同的字节序，这样对于同一份以宽字符存储的数据来说，可能在不同的平台上得到不同的结果。</p>
<p>- 与已有I/O设备兼容性差<br />
	以往的设备都是面向字节设备的，处理ASCII字符以及由ASCII扩展而来的多字节字符问题不大。但对于由两个字节或四个字节组成的宽字符来说，显然有些力不从心了。</p>
<p>其次由于各个国家和地区纷纷独立制定多字节字符标准，导致了不同字符集之间的不兼容。比如：GBK编码中&quot;中&quot;字的编码是D6D0，而BIG5中&quot;中&quot;的编码则是A4A4。这样一来，一些涉及文本处理的程序，比如文本编辑器，就需要花费大量的工作在了不同编码间的相互识别和解析上。这时一些组织站了出来，试图建立可以容纳全球所有语言字符的统一字符集，Unicode/ISO 10646（为方便期间，二者之间的一些差异这里就忽略不计了，以下统称Unicode）因此诞生。Unicode简单来说就是一组标量数字集合，其中每个数字映射地球上的一个唯一字符。以往大家对于Unicode的理解就是用2个字节(Unicode-16，UCS-2)或4个字节(Unicode- 32, UCS-4)进行编码的宽字符。实则不然，这些理解只是其一，因为最初使用2个字节（后来发现2个字节是严重不足的）或4个字节可以一一映射 Unicode字符集合，编码值就是Unicode字符对应的Unicode字符集表中的那个数字。但是用宽字符作为Unicode底层编码的实现方式显然也会遇到上面所说的各种问题；于是乎基于多字节编码的Unicode实现出现了，最著名的莫过于utf8了，当然还有utf16和utf32。没错，utf8字符是一种多字节字符，utf8与unicode表示字符个数的能力上是等同的。Unicode字符可以与utf8字符做一一对应的转换。和其它多字节编码方案一样，utf8也兼容ASCII编码，也是面向字节的，utf8可以完全替代各个国家地区自己制定的那些私有编码方案。事实上，目前 utf8已经是全球字符编码的事实标准（de facto standard）了。</p>
<p>我们现在来实现这样一个程序：它可以在不同locale下输出foo.dat文件中的字符个数和字节个数，其中foo.dat文件中存储的数据的编码方式为locale指定的。我们有两个思路：<br />
	1、假设我们拥有所有locale的字符解析库，我们可以将数据从文件中读取出来后，用当前locale对应的字符解析库对数据进行解析，得到字符的个数；<br />
	2、利用locale技术，将文件中的数据读取后转换为宽字符，再计算宽字符的个数，即为foo.dat文件中字符的个数。</p>
<p>我们粗略对比以下这两种思路，优劣立见。利用locale技术，你无需了解任何有关目标主机字符编码的细节，也无需自携带规模庞大的字符解析库，另外无需做任何修改即可支持新增的locale配置。下面就是一个利用locale技术进行字节/字符计数的例子（仅仅是个例子哦），这个程序可以在不同locale下输出foo.dat中的字符个数和字节个数：</p>
<p>/* wc.c */<br />
	int main(int argc, const char *argv[])<br />
	{<br />
	&nbsp;&nbsp;&nbsp; int bytes = 0;<br />
	&nbsp;&nbsp;&nbsp; int words = 0;</p>
<p>&nbsp;&nbsp;&nbsp; setlocale(LC_ALL, &quot;&quot;);<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;Current locale is %s!\n&quot;, setlocale(LC_ALL, NULL));</p>
<p>&nbsp;&nbsp;&nbsp; FILE *fp = NULL;</p>
<p>&nbsp;&nbsp;&nbsp; fp = fopen(&quot;foo.dat&quot;, &quot;rb&quot;);<br />
	&nbsp;&nbsp;&nbsp; if (!fp) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;failed to open foo.dat, err: %d\n&quot;, errno);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -1;<br />
	&nbsp;&nbsp;&nbsp; }</p>
<p>&nbsp;&nbsp;&nbsp; char mbs_buf[1024];<br />
	&nbsp;&nbsp;&nbsp; wchar_t wcs_buf[100];<br />
	&nbsp;&nbsp;&nbsp; mbstate_t s;<br />
	&nbsp;&nbsp;&nbsp; size_t n;<br />
	&nbsp;&nbsp;&nbsp; const char *p;<br />
	&nbsp;&nbsp;&nbsp; memset(mbs_buf, 0, sizeof(mbs_buf));</p>
<p>&nbsp;&nbsp;&nbsp; while (NULL != fgets(mbs_buf, 1024, fp)) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; memset(&#038;s, 0, sizeof(s));<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; memset(wcs_buf, 0, sizeof(wcs_buf));<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; p = mbs_buf;</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; n = mbsrtowcs(wcs_buf, &#038;p, sizeof(wcs_buf), &#038;s);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (n == -1) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;failed to convert multi-bytes character to wide character, err: %d\n&quot;, errno);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -1;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } else {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; bytes += strlen(mbs_buf);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; words += wcslen(wcs_buf);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; memset(mbs_buf, 0, sizeof(mbs_buf));<br />
	&nbsp;&nbsp;&nbsp; }</p>
<p>&nbsp;&nbsp;&nbsp; printf(&quot;bytes = %d\n&quot;, bytes);<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;words = %d\n&quot;, words);</p>
<p>&nbsp;&nbsp;&nbsp; fclose(fp);<br />
	&nbsp;&nbsp;&nbsp; return 0;<br />
	}</p>
<p>分别在具有两个不同locale的账户下制作foo.dat:<br />
	cat &gt; foo.dat<br />
	中华人民共和国^D (输入Ctrl+D)</p>
<p>在locale为gb18030下的测试结果是:<br />
	Current locale is zh_CN.GB18030!<br />
	bytes = 14<br />
	words = 7</p>
<p>在locale为utf8下的测试结果是:<br />
	Current locale is zh_CN.utf8!<br />
	bytes = 21<br />
	words = 7</p>
<p>在C语言中，除了显式调用库函数在宽字符和多字节字符之间转换外，C语言本身还有一些隐式的转换值得注意。</p>
<p>首先就是宽字符的转换。如果你在源文件中用L&quot;XXX&quot;给一个wchar_t数组赋值，那么Gcc会默认将XXX看成是utf8编码的字符串。如果你的源文件确实是utf8编码的，那么类似wchar_t w[] = L&quot;中国&quot;则相当于编译器做了一次utf8到unicode-32的转换;但是如果你的源码文件不是utf8编码的，比如是gb18030的，那么编译器将提示错误：&ldquo;converting to execution character set：无效或不完整的多字节字符或宽字符&rdquo;。这时需要你通过Gcc命令选项显式指定源码字符集类型：-finput-charset=&#039;gb18030&#039;。</p>
<p>其次利用%ls输出宽字符串时也需要注意隐式转换，看下面例子：</p>
<p>/* widechar.c, 该文件采用utf8编码 */<br />
	int main(int argc, const char *argv[])<br />
	{<br />
	&nbsp;&nbsp; wchar_t w[] = L&quot;中国&quot;;<br />
	&nbsp;&nbsp; printf(&quot;%ls\n&quot;, w);<br />
	&nbsp;&nbsp; return 0;<br />
	}</p>
<p>编译ok，但执行后发现无法输出&ldquo;中国&quot;二字。printf在%ls下支持输出宽字符串，但是也是需要显式指定locale的，否则当前LC_ALL就等于&quot;C&quot;，在&quot;C&quot;locale下printf显然无法将宽字符&quot;中国&quot;成功转换为utf8编码并输出。我们稍作修改：</p>
<p>/* widechar.c, 该文件采用utf8编码 */<br />
	int main(int argc, const char *argv[])<br />
	{<br />
	&nbsp;&nbsp; setlocale(LC_ALL, &quot;&quot;);<br />
	&nbsp;&nbsp; wchar_t w[] = L&quot;中国&quot;;<br />
	&nbsp;&nbsp; printf(&quot;%ls\n&quot;, w);<br />
	&nbsp;&nbsp; return 0;<br />
	}</p>
<p>通过setlocale(LC_ALL, &quot;&quot;)将locale指定为用户当前locale，这样我们就可以顺利见到&quot;中国&quot;字样了。printf做了一次宽字符到utf8的转换后，再将utf8字符串打印到控制台上，为我们所见。</p>
<p>最后，C99支持在源码中使用通用字符名(Universal Character Name, UCN)来表示任何扩展字符集中的字符。利用\U或\u来指定一个Unicode字符，但是注意千万不要以为宽字符和\U0000nnnn或\unnnn是等价的。下面这么做是无法达到你的预期的：</p>
<p>wchar_t w = &#039;\u4e2d&#039;; /* 4e2d是&quot;中&quot;字的Unicode编码 */</p>
<p>如果按我们的预期，w中的4个字节应该依次是0&#215;00，0&#215;00，0x4e和0x2d。但经过实际探测，我们得到的却是0&#215;00、0xe4、0xb8和0xad，这恰恰是&quot;中&quot;的utf8编码。而且编译器还在这一行给出了警告：warning: multi-character character constant。这里也是一种隐式转换，使用UCN表示的Unicode字符将首先被按照执行字符集做转换后再作为右值，此时它就和一个多字节字符串无异，所以这里使用char mbs[] = &quot;\u4e2d&quot;才是正确的。我们可以将\u或\U作为转义字符来看待，这样在真正的编译开始之前，当Compiler处理所有转义字符及字符串时，这些字符和字符串将被预先转换为执行字符集中对应的字符，正如\u4e2d被转换为e4b8ad。</p>
<p style='text-align:left'>&copy; 2011, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2011/07/01/also-talk-about-the-internationalization-support-in-c/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
