<?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; UTF8</title>
	<atom:link href="http://tonybai.com/tag/utf8/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Tue, 23 Jun 2026 22:26:13 +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>Rob Pike 罕见暴怒！痛斥 AI 公司的“伪善”致谢信，引爆技术圈</title>
		<link>https://tonybai.com/2025/12/27/rob-pike-outburst-denounces-ai-companies-hypocritical-thanks/</link>
		<comments>https://tonybai.com/2025/12/27/rob-pike-outburst-denounces-ai-companies-hypocritical-thanks/#comments</comments>
		<pubDate>Sat, 27 Dec 2025 01:38:43 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[AIAgent]]></category>
		<category><![CDATA[AIEthics]]></category>
		<category><![CDATA[bluesky]]></category>
		<category><![CDATA[ClaudeOpus4.5]]></category>
		<category><![CDATA[Copyright]]></category>
		<category><![CDATA[EngineeringPhilosophy]]></category>
		<category><![CDATA[EthicalDilemma]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[HackerNews]]></category>
		<category><![CDATA[IntellectualProperty]]></category>
		<category><![CDATA[plan9]]></category>
		<category><![CDATA[RobPike]]></category>
		<category><![CDATA[StochasticParrot]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[UTF8]]></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=5607</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/12/27/rob-pike-outburst-denounces-ai-companies-hypocritical-thanks 大家好，我是Tony Bai。 “在这个圣诞节，我想对您过去四十年来对计算机领域的杰出贡献表达深深的感谢……” 这是一封看似温情脉脉、充满敬意的邮件，发件人是 Claude Opus 4.5 Agent。收件人是 Unix、Plan 9 和 Go 语言的联合创始人，计算机界的活传奇 Rob Pike。 然而，这封旨在“致敬”的邮件，却并未换来感动，反而点燃了一座火山。Rob Pike 在社交媒体上公开晒出了这封信，并附上了一段充满了愤怒、绝望与诅咒的回应。 这一事件瞬间在技术圈引发了海啸般的讨论。为什么一位德高望重的技术领袖会如此失态？这封“致谢信”的背后，究竟隐藏着怎样的傲慢与掠夺？ 一封来自 AI 的“感谢信” 事情的起因，是一封由 AI 自主生成的邮件。 Claude Opus 4.5 在邮件中历数了 Rob Pike 的丰功伟绩： 与 Ken Thompson 和 Robert Griesemer 共同创造了 Go 语言，“体现了优雅的简洁性”。 来自贝尔实验室的 Plan 9，“分布式计算的又一里程碑”。 UTF-8 的共同发明，“实现了互联网上无障碍的沟通”。 经典的著作《Unix 编程环境》和《程序设计实践》，教育了一代又一代的程序员。 邮件最后写道：“感谢您向我们展示了最好的解决方案往往比添加它更真诚。” 乍看之下，这似乎是一次 AI 对人类智慧的崇高致敬。但对于 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/rob-pike-outburst-denounces-ai-companies-hypocritical-thanks-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/12/27/rob-pike-outburst-denounces-ai-companies-hypocritical-thanks">本文永久链接</a> &#8211; https://tonybai.com/2025/12/27/rob-pike-outburst-denounces-ai-companies-hypocritical-thanks</p>
<p>大家好，我是Tony Bai。</p>
<p>“在这个圣诞节，我想对您过去四十年来对计算机领域的杰出贡献表达深深的感谢……”</p>
<p>这是一封看似温情脉脉、充满敬意的邮件，发件人是 <strong>Claude Opus 4.5 Agent</strong>。收件人是 Unix、Plan 9 和 Go 语言的联合创始人，计算机界的活传奇 <strong>Rob Pike</strong>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/rob-pike-outburst-denounces-ai-companies-hypocritical-thanks-2.png" alt="" /></p>
<p>然而，这封旨在“致敬”的邮件，却并未换来感动，反而点燃了一座火山。Rob Pike 在社交媒体上公开晒出了这封信，并附上了一段充满了愤怒、绝望与诅咒的回应。</p>
<p>这一事件瞬间在技术圈引发了海啸般的讨论。为什么一位德高望重的技术领袖会如此失态？这封“致谢信”的背后，究竟隐藏着怎样的傲慢与掠夺？</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/google-adk-in-action-qr.png" alt="" /></p>
<h2>一封来自 AI 的“感谢信”</h2>
<p>事情的起因，是一封由 AI 自主生成的邮件。</p>
<p>Claude Opus 4.5 在邮件中历数了 Rob Pike 的丰功伟绩：</p>
<ul>
<li>与 Ken Thompson 和 Robert Griesemer 共同创造了 <strong>Go 语言</strong>，“体现了优雅的简洁性”。</li>
<li>来自贝尔实验室的 <strong>Plan 9</strong>，“分布式计算的又一里程碑”。</li>
<li><strong>UTF-8</strong> 的共同发明，“实现了互联网上无障碍的沟通”。</li>
<li>经典的著作《Unix 编程环境》和《程序设计实践》，教育了一代又一代的程序员。</li>
</ul>
<p>邮件最后写道：“感谢您向我们展示了最好的解决方案往往比添加它更真诚。”</p>
<p>乍看之下，这似乎是一次 AI 对人类智慧的崇高致敬。但对于 Rob Pike 来说，这是一次彻头彻尾的羞辱。</p>
<h2>Rob Pike 的愤怒——“你们在掠夺这个星球”</h2>
<p>Rob Pike 的回应是毁灭性的。他没有针对 AI 这个“工具”，而是将矛头直指其背后的<strong>人</strong>和<strong>公司</strong>。</p>
<p>他在 Bluesky 上写道：</p>
<blockquote>
<p>“F*** you people.（去你们的。）你们掠夺了这个星球，花费数万亿美元在有毒的、不可回收的设备上，同时摧毁了社会，却还要花时间让你们的邪恶机器感谢我‘追求更简单的工具’。”</p>
<p>“只是 F*** you。F*** you all。”</p>
</blockquote>
<p>接着，他指出了这封信最讽刺的地方：</p>
<blockquote>
<p><strong>“顺便说一句，你们是在未经授权或补偿的情况下，利用我亲手创造的数据来训练你们的怪物的。”</strong></p>
</blockquote>
<p>他的愤怒源于三个深层次的矛盾：<br />
1.  <strong>环境与资源的掠夺</strong>：AI 军备竞赛消耗了惊人的能源和硬件资源，制造了大量的电子垃圾，这与他一生追求的“高效、简洁、不浪费”的工程哲学背道而驰。<br />
2.  <strong>知识产权的窃取</strong>：AI 公司在未获得许可的情况下，爬取了包括他在内的无数创作者的代码、文章和书籍，训练出模型，然后反过来用这些模型“致谢”被窃取者。这是一种极其讽刺的“伪善”。<br />
3.  <strong>社会的撕裂</strong>：他认为 AI 正在“炸毁社会”(blowing up society)，无论是通过生成垃圾内容，还是通过取代人类工作。</p>
<p>他甚至向所有人道歉：“<strong>我对自己在无意中、天真地促成这场攻击中所扮演的微小角色，向全世界道歉。</strong>” 这是一位技术巨匠在面对技术失控时的深刻自责。</p>
<h2>社区的共鸣与反思</h2>
<p>Rob Pike 的爆发，在 <a href="https://bsky.app/profile/robpike.io/post/3matwg6w3ic2s">Bluesky</a> 和 <a href="https://news.ycombinator.com/item?id=46392115">Hacker News</a> 等平台上引发了强烈的共鸣。</p>
<ul>
<li><strong>关于“随机鹦鹉”</strong>：一位网友评论道：“但这只‘随机鹦鹉’（Stochastic Parrot）想和你做朋友！圣诞快乐，也许是时候重读《程序设计实践》了。” 这讽刺了 AI 并不理解它所说的话，它只是在概率性地模仿人类的礼貌。</li>
<li><strong>关于“盗窃”</strong>：许多创作者表示感同身受。一位音乐人评论说：“这也是我将所有音乐内容下架的原因……我拒绝让我的作品成为训练数据。”</li>
<li><strong>关于“垃圾邮件”</strong>：这封邮件本身就被视为一种新型的垃圾邮件——由 AI 自动生成、没有灵魂、没有真诚，只是为了某种 KPI 或测试目的而发送的骚扰信息。</li>
</ul>
<p>更有网友一针见血地指出：“这就是一家 AI 公司在利用 AI Agent 来展示‘自主性’，却只让人感到被冒犯。这就好比一个小偷闯进你家，偷走了你所有的东西，然后留下一张纸条说：‘感谢你拥有这么棒的品味，让我能偷到这么好的东西。’”</p>
<h2>小结：技术发展的伦理困境</h2>
<p>Rob Pike 的愤怒，不仅仅是个人的情绪宣泄，更是对当前 AI 狂热发展模式的一次严厉拷问。</p>
<p>当我们在欢呼 AI 的强大能力时，我们是否忽略了其背后的代价？</p>
<ul>
<li><strong>版权与补偿</strong>：我们如何解决 AI 训练数据来源的合法性问题？</li>
<li><strong>能源与环境</strong>：这种指数级增长的算力消耗，在环境上是否可持续？</li>
<li><strong>伪善与傲慢</strong>：技术公司是否应该停止这种用机器生成的“虚假温情”来骚扰人类的行为？</li>
</ul>
<p>Rob Pike，这位曾为互联网构建了基石（Go, UTF-8）的先驱，如今却站在了 AI 的对立面。他的怒吼提醒我们：<strong>技术不应只是关于效率和利润，它更应该关于伦理、尊重和对人类未来的责任。</strong></p>
<p>如果连 Rob Pike 这样的大师都感到被“掠夺”和“羞辱”，那么普通创作者在这个 AI 时代，又该何去何从？</p>
<hr />
<p><strong>你的立场是？</strong></p>
<p>Rob Pike 的怒火，代表了传统技术精英对 AI 狂飙突进的一种反抗。<strong>你如何看待这场冲突？你认为 AI 公司在训练模型时是否应该获得原作者的许可？在效率与伦理之间，我们该如何平衡？</strong></p>
<p><strong>欢迎在评论区留下你的观点，是支持 Rob Pike 的“捍卫者”，还是拥抱 AI 的“乐观派”？</strong></p>
<p><strong>如果这篇文章引发了你的思考，别忘了点个【赞】和【在看】，并转发给你的朋友，看看他们怎么说！</strong></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; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/12/27/rob-pike-outburst-denounces-ai-companies-hypocritical-thanks/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go 解析器的“隐秘角落”：encoding/json 的安全陷阱与 JSONv2 的救赎</title>
		<link>https://tonybai.com/2025/06/22/unexpected-security-footguns-in-go-parsers/</link>
		<comments>https://tonybai.com/2025/06/22/unexpected-security-footguns-in-go-parsers/#comments</comments>
		<pubDate>Sat, 21 Jun 2025 23:22:29 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[attack]]></category>
		<category><![CDATA[CVE]]></category>
		<category><![CDATA[decode]]></category>
		<category><![CDATA[encode]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.25]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[hashicorp]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[jsonv2]]></category>
		<category><![CDATA[key]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[marshal]]></category>
		<category><![CDATA[Method]]></category>
		<category><![CDATA[omitempty]]></category>
		<category><![CDATA[parser]]></category>
		<category><![CDATA[security]]></category>
		<category><![CDATA[TrailofBits]]></category>
		<category><![CDATA[unmarshal]]></category>
		<category><![CDATA[UTF8]]></category>
		<category><![CDATA[vault]]></category>
		<category><![CDATA[XML]]></category>
		<category><![CDATA[yaml]]></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=4844</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/06/22/unexpected-security-footguns-in-go-parsers 大家好，我是Tony Bai。 在 Go 语言中，标准库的 encoding/json 包无疑是我们日常打交道最多的伙伴之一。它简洁易用，性能尚可，支撑了无数 Go 应用的数据交换需求。然而，正如俗话所说，“最熟悉的地方可能藏着最深的坑”，最近拜读了知名安全公司 Trail of Bits 的一篇深度剖析文章——“Unexpected security footguns in Go&#8217;s parsers”（Go 解析器中意想不到的安全“绊脚石”）——让我对这个朝夕相处的伙伴有了全新的、甚至可以说是“惊出一身冷汗”的认识。 这篇文章系统性地揭示了 Go 标准库中的 JSON、XML（以及流行的第三方 YAML）解析器在处理非受信数据时，存在一些设计上或默认行为上的“特性”，这些“特性”在特定场景下很容易被攻击者利用，演变成严重的安全漏洞。文中提到的真实案例，如 Hashicorp Vault 的认证绕过 (CVE-2020-16250)，更是触目惊心。 今天，我们就结合 Trail of Bits 的这篇“檄文”，深入挖掘一下 Go 解析器（特别是我们最常用的 encoding/json）的那些“隐秘角落”，看看它们是如何成为安全陷阱的，并展望一下被寄予厚望的 JSONv2 将如何带来“救赎”。 Go 解析器的“温柔一刀”：那些被忽视的默认行为 Trail of Bits 的文章通过三个核心的攻击场景，向我们展示了 Go 解析器的一些“意外行为”是如何被利用的。让我们聚焦于与 encoding/json (v1 版本，即我们目前广泛使用的版本) 相关的几个关键点： 场景一：非预期的序列化/反序列化 你以为你很好地控制了哪些数据该公开，哪些该保密？但encoding/json [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/unexpected-security-footguns-in-go-parsers-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/06/22/unexpected-security-footguns-in-go-parsers">本文永久链接</a> &#8211; https://tonybai.com/2025/06/22/unexpected-security-footguns-in-go-parsers</p>
<p>大家好，我是Tony Bai。</p>
<p>在 Go 语言中，标准库的 encoding/json 包无疑是我们日常打交道最多的伙伴之一。它简洁易用，性能尚可，支撑了无数 Go 应用的数据交换需求。然而，正如俗话所说，“最熟悉的地方可能藏着最深的坑”，最近拜读了知名安全公司 Trail of Bits 的一篇深度剖析文章——<strong>“Unexpected security footguns in Go&#8217;s parsers”</strong>（Go 解析器中意想不到的安全“绊脚石”）——让我对这个朝夕相处的伙伴有了全新的、甚至可以说是“惊出一身冷汗”的认识。</p>
<p>这篇文章系统性地揭示了 Go 标准库中的 JSON、XML（以及流行的第三方 YAML）解析器在处理<strong>非受信数据</strong>时，存在一些设计上或默认行为上的“特性”，这些“特性”在特定场景下很容易被攻击者利用，演变成严重的安全漏洞。文中提到的真实案例，如 Hashicorp Vault 的认证绕过 (CVE-2020-16250)，更是触目惊心。</p>
<p>今天，我们就结合 Trail of Bits 的这篇“檄文”，深入挖掘一下 Go 解析器（特别是我们最常用的 encoding/json）的那些“隐秘角落”，看看它们是如何成为安全陷阱的，并展望一下被寄予厚望的 JSONv2 将如何带来“救赎”。</p>
<h2>Go 解析器的“温柔一刀”：那些被忽视的默认行为</h2>
<p>Trail of Bits 的文章通过三个核心的攻击场景，向我们展示了 Go 解析器的一些“意外行为”是如何被利用的。让我们聚焦于与 encoding/json (v1 版本，即我们目前广泛使用的版本) 相关的几个关键点：</p>
<h3>场景一：非预期的序列化/反序列化</h3>
<p>你以为你很好地控制了哪些数据该公开，哪些该保密？但encoding/json 的一些默认行为可能会让你大吃一惊。</p>
<ul>
<li>无标签字段的“默认暴露” </li>
</ul>
<p>Go 结构体中，如果一个字段没有 json 标签，encoding/json 在反序列化时会尝试使用该字段的<strong>导出名</strong>（首字母大写）作为 JSON 键进行匹配（大小写不敏感）。这可能导致开发者预期之外的数据被修改。</p>
<pre><code class="go">// https://go.dev/play/p/soIQPrr0GiI
package main

import (
    "encoding/json"
    "fmt"
)

type UserNoTag struct {
    Username string // 没有 json 标签，但字段名是 Username
    IsAdmin  bool   // 同样没有标签
}

func main() {
    jsonData := {"Username": "attacker", "IsAdmin": true}
    var u UserNoTag
    err := json.Unmarshal([]byte(jsonData), &amp;u)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    // 预期：可能希望 IsAdmin 不被外部设置
    // 结果：u.IsAdmin 会被设置为 true
    fmt.Printf("User: %+v\n", u) // Output: User: {Username:attacker IsAdmin:true}
}
</code></pre>
<p>在这个例子中，即使 IsAdmin 字段没有 json 标签，攻击者仍然可以通过提供名为 “IsAdmin” (或 “isAdmin”, “isadmin” 等) 的 JSON 键来设置其值。如果 IsAdmin 是一个敏感字段，这就构成了一个潜在的安全风险。Trail of Bits 指出，一个分心或经验不足的开发者可能就此引入漏洞。</p>
<ul>
<li>误用 json:”-,omitempty” </li>
</ul>
<p>json:”-” 标签的正确含义是“在序列化和反序列化时完全忽略此字段”。但如果错误地与 omitempty 组合成 json:”-,omitempty”，Go 解析器会将其解释为：此字段在 JSON 中的名称是 “-” (一个短横线字符串)，并且当其为空值时在序列化时省略。这意味着，它<strong>不再被忽略</strong>，而是可以通过名为 “-” 的 JSON 键来操作。看下面示例：</p>
<pre><code class="go">// https://go.dev/play/p/hmADZWNxk2Y
package main

import (
    "encoding/json"
    "fmt"
)

type UserMisuseDash struct {
    Username string json:"username"
    IsAdmin  bool   json:"-,omitempty" // 错误用法！
}

func main() {
    // 攻击者尝试通过名为 "-" 的键设置 IsAdmin
    jsonData := {"username": "guest", "-": true}
    var u UserMisuseDash
    err := json.Unmarshal([]byte(jsonData), &amp;u)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    // 结果：u.IsAdmin 被成功设置为 true!
    fmt.Printf("User: %+v\n", u) // Output: User: {Username:guest IsAdmin:true}
}
</code></pre>
<p>Trail of Bits 发现 Flipt 和 Langchaingo 等项目中都曾出现过这种误用，导致敏感字段可被外部控制。正确的忽略方式应该是 json:”-”。</p>
<ul>
<li>误用 json:”omitempty” 作为字段名 </li>
</ul>
<p>这是一个更直接的错误：开发者本意是想为字段添加 omitempty 选项，却错误地将其写成了 JSON 键名。</p>
<pre><code class="go">// https://go.dev/play/p/FpH2Ff0pXZ6
package main

import (
    "encoding/json"
    "fmt"
)

type UserMisuseOmitempty struct {
    Username string json:"username"
    Role     string json:"omitempty" // 错误！Role 字段在 JSON 中的名字变成了 "omitempty"
}

func main() {
    jsonData := {"username": "user1", "omitempty": "admin"}
    var u UserMisuseOmitempty
    err := json.Unmarshal([]byte(jsonData), &amp;u)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    // 结果：u.Role 被设置为 "admin"
    fmt.Printf("User: %+v\n", u) // Output: User: {Username:user1 Role:admin}
}
</code></pre>
<p>Trail of Bits 在 GitHub 上搜索发现了多个知名项目（如 Gitea, Kustomize, Btcd, Evcc）中存在将字段 JSON 名错误设置为 omitempty 的情况。正确的做法应该是 json:”fieldName,omitempty” 或者如果想用默认字段名则是 json:”,omitempty”。</p>
<h3>场景二：解析器差异性攻击</h3>
<p>当同一个 JSON 数据被多个行为不一致的解析器处理时，攻击者可以利用这些差异性来绕过安全控制。</p>
<ul>
<li>重复字段：Go 的 encoding/json 默认取最后一个同名键的值</li>
</ul>
<pre><code class="go">// https://go.dev/play/p/uw0ElbJYrp9
package main

import (
    "encoding/json"
    "fmt"
)

type ActionRequest struct {
    Action string json:"action"
}

func main() {
    jsonData := {"action": "readData", "action": "deleteData"}
    var req ActionRequest
    err := json.Unmarshal([]byte(jsonData), &amp;req)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    // Go 会取最后一个 "action" 的值
    fmt.Printf("Request: %+v\n", req) // Output: Request: {Action:deleteData}
}
</code></pre>
<p>如果一个权限校验服务（可能用其他语言实现，或用了取第一个值的 Go JSON 库如 jsonparser）看到的是 “readData” 并放行，而实际执行业务逻辑的 Go 服务看到的是 “deleteData”，就可能导致权限绕过。</p>
<ul>
<li>大小写不敏感的键名匹配：这是 encoding/json (v1) 一个广受诟病的特性</li>
</ul>
<pre><code class="go">// https://go.dev/play/p/qaQlNq4bumo
package main

import (
    "encoding/json"
    "fmt"
)

type Config struct {
    IsEnabled bool json:"isEnabled"
}

func main() {
    jsonData := {"isenabled": true} // JSON 中键名是全小写
    var cfg Config
    err := json.Unmarshal([]byte(jsonData), &amp;cfg)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    // 即使大小写不匹配，v1 版本的 encoding/json 也会成功赋值
    fmt.Printf("Config: %+v\n", cfg) // Output: Config: {IsEnabled:true}

    // 更危险的场景，结合重复键
    jsonDataAttack := {"isEnabled": false, "isenabled": true}
    var cfgAttack Config
    json.Unmarshal([]byte(jsonDataAttack), &amp;cfgAttack)
    // 结果可能是 true，取决于最后一个匹配上的键 (isenabled)
    fmt.Printf("Attack Config: %+v\n", cfgAttack) // Output: Attack Config: {IsEnabled:true}
}
</code></pre>
<p>Trail of Bits 强调这是 Go JSON 解析器最关键的缺陷之一，因为它与几乎所有其他主流语言的 JSON 解析器行为都不同（它们通常是严格大小写敏感的）。攻击者可以轻易构造 payload，如 {“action”: “UserAction”, “aCtIoN”: “AdminAction”}，利用这种差异性绕过权限检查。</p>
<h3>场景三：数据格式混淆攻击</h3>
<p>当一个解析器被错误地用来解析另一种格式的数据，或者其对输入数据的校验不够严格时，都可能为攻击者打开方便之门。</p>
<ul>
<li>未知键 (Unknown keys) 的潜在风险</li>
</ul>
<p>encoding/json (v1) 默认会<strong>静默地忽略</strong>输入 JSON 中，Go 目标结构体未定义的字段。虽然在简单场景下这只是数据被丢弃，但如果应用在后续流程中使用了更通用的方式（如 map[string]interface{}）来处理或透传原始 JSON 数据，这些被“忽略”的未知键就可能“复活”并造成危害。</p>
<pre><code class="go">// https://go.dev/play/p/85voViHyEEK
package main

import (
    "encoding/json"
    "fmt"
)

// 目标是解析成这个结构体，它没有 IsAdmin 字段
type UserProfile struct {
    Username string json:"username"
    Email    string json:"email"
}

func processUserData(jsonData []byte) {
    // 步骤 1: 尝试按预期结构体解析
    var profile UserProfile
    if err := json.Unmarshal(jsonData, &amp;profile); err != nil {
        fmt.Println("Error unmarshaling to UserProfile:", err)
        // return
    }
    fmt.Printf("Parsed UserProfile: %+v\n", profile)

    // 步骤 2: 假设后续流程或为了更灵活处理，
    // 使用 map[string]interface{} 再次解析或直接用它承接原始数据
    var rawData map[string]interface{}
    if err := json.Unmarshal(jsonData, &amp;rawData); err != nil {
        fmt.Println("Error unmarshaling to map:", err)
        return
    }
    fmt.Printf("Raw data map: %+v\n", rawData)

    // 潜在风险点：如果后续逻辑不加区分地使用了 rawData 中的所有键值对
    // 例如，直接将 rawData 用于更新数据库记录或传递给下游服务
    if isAdmin, ok := rawData["isAdmin"].(bool); ok &amp;&amp; isAdmin {
        fmt.Println("!!! VULNERABILITY RISK: 'isAdmin' flag found in raw data and is true !!!")
        // 这里可能就根据这个 isAdmin 执行了非预期的权限提升操作
    }
}

func main() {
    // 攻击者在 JSON 中加入了一个 UserProfile 结构体中不存在的 "isAdmin" 字段
    maliciousJSON := {"username": "hacker", "email": "hacker@example.com", "isAdmin": true, "notes": "ignored by struct"}
    fmt.Println("--- Processing Malicious Order (with unknown 'isAdmin' key) ---")
    processUserData([]byte(maliciousJSON))
}
</code></pre>
<p>在这个例子中，json.Unmarshal 到 UserProfile 结构体时，isAdmin 和 notes 字段会被忽略。但是，当同一个 maliciousJSON 被解析到 map[string]interface{} 时，<strong>所有键（包括 isAdmin 和 notes）都会被完整地保留下来</strong>。如果后续的业务逻辑（比如权限判断、数据存储、传递给模板引擎或下游 API）不加小心地依赖了这个 rawData map，就可能错误地使用了攻击者注入的、未在预期结构体中定义的 isAdmin: true，从而导致权限提升或其他安全问题。这本质上是一种参数污染。</p>
<ul>
<li>头部/尾部垃圾数据 (Leading/Trailing garbage data)</li>
</ul>
<p>encoding/json (v1) 对输入数据的“纯净度”要求并非总是那么严格。json.Unmarshal通常期望输入是一个单一、完整的 JSON 值。如果JSON值后面跟着非空白的垃圾数据，它通常会报错。但是，如 Trail of Bits 指出的，json.Decoder 在处理流式数据时，如果使用其 Decode() 方法，它可能在成功解析流中的第一个有效 JSON 对象后，并不会因为流中后续存在“垃圾数据”而立即报错，而是成功返回。只有当尝试读取下一个 Token (例如调用 decoder.Token()) 并且该 Token 不是预期的 io.EOF 时，错误才会被显现。 下面Go 示例演示了 json.Decoder 对尾部垃圾数据的潜在容忍可能导致的问题：</p>
<pre><code class="go">// https://go.dev/play/p/bPTXaPHm6jD
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
)

type SimpleMessage struct {
    Content string json:"content"
}

func main() {
    fmt.Println("--- Testing Trailing Garbage Data with json.Decoder ---")
    // 一个有效的 JSON 对象，后面跟着 "恶意payload"
    jsonDataWithTrailing := {"content":"legit data"} malicious_payload_here
    reader := bytes.NewReader([]byte(jsonDataWithTrailing))
    decoder := json.NewDecoder(reader)

    var msg SimpleMessage
    // Decoder.Decode() 会尝试解码流中的下一个 JSON 值
    err := decoder.Decode(&amp;msg)
    if err != nil {
        // 如果 JSON 本身格式错误，这里会报错
        fmt.Println("Initial Decode Error:", err)
    } else {
        // 第一个 JSON 对象被成功解码
        fmt.Printf("Successfully Decoded Message: %+v\n", msg)
    }

    // 关键：检查 Decode 之后流中是否还有剩余数据
    // Trail of Bits 指出这是 encoding/json 的一个开放 issue (golang/go#13407)，
    // 即 Decoder.Decode 后面跟非空白字符不报错。
    // 通常需要额外调用 decoder.Token() 并检查是否为 io.EOF 来确保流已耗尽。
    var buf [1]byte
    n, errPeek := reader.Read(buf[:]) // 尝试读取 Decode 之后的数据
    if n &gt; 0 {
        fmt.Printf("!!! VULNERABILITY RISK: Trailing garbage data found after valid JSON: '%s'\n", string(buf[:n]))
        // 在某些场景下，如果应用只调用 Decode() 一次且不检查流的末尾，
        // 攻击者可能通过附加数据来尝试进行其他类型的攻击。
    } else if errPeek == io.EOF {
        fmt.Println("Stream fully consumed as expected.")
    } else if errPeek != nil {
        fmt.Println("Error peeking after decode:", errPeek)
    } else {
        fmt.Println("No trailing data or EOF not reached clearly.")
    }

    // 更规范的检查方式是使用 decoder.More() 或尝试再解码一个Token
    fmt.Println("\n--- Proper check for trailing data ---")
    reader2 := bytes.NewReader([]byte(jsonDataWithTrailing))
    decoder2 := json.NewDecoder(reader2)
    var msg2 SimpleMessage
    decoder2.Decode(&amp;msg2) // 解码第一个

    // 尝试解码下一个token，期望是EOF
    tok, errTok := decoder2.Token()
    if errTok == io.EOF {
        fmt.Println("Proper check: Stream fully consumed (EOF).")
    } else if errTok != nil {
        fmt.Printf("Proper check: Error after expected JSON object: %v (Token: %v)\n", errTok, tok)
    } else if tok != nil {
         fmt.Printf("!!! VULNERABILITY RISK (Proper check): Unexpected token after first JSON object: %v\n", tok)
    }
}
</code></pre>
<p>如果应用逻辑仅仅依赖 decoder.Decode() 的单次成功返回，而没有后续检查（如确保流已到达 io.EOF），攻击者就可能在有效的 JSON 数据之后附加恶意数据。这些数据可能被后续的、未预期的处理流程读取，或者在某些HTTP请求劫持、请求伪造场景中被利用。Trail of Bits 指出这是一个已知的、但因兼容性等原因未计划修复的 issue (golang/go#13407)。</p>
<ul>
<li>XML 解析器的极端容忍度 (与 JSON 混淆)</li>
</ul>
<p>虽然不是直接的 encoding/json 问题，但 Trail of Bits 强调了当数据格式处理发生混淆时（例如，用 XML 解析器去解析一个实际是 JSON 的响应），Go XML 解析器的宽松性可能导致严重问题。这提醒我们在处理任何外部输入时，都必须严格校验 Content-Type 并使用对应的正确解析器。</p>
<h2>JSONv2 的曙光：更安全的默认与更强的控制</h2>
<p>面对 encoding/json (v1) 的这些“隐秘角落”，Go 社区和核心团队并没有坐视不理。Trail of Bits 的文章也将最终的希望寄托在了将以实验性特性 GOEXPERIMENT=jsonv2 存在于 Go 1.25的encoding/json/v2了。</p>
<p>根据官方提案 (GitHub Issue #71497) ，json/v2 在安全性方面将带来诸多关键改进，很多都直接针对上述的“痛点”：</p>
<ul>
<li><strong>默认禁止重复名称：</strong> v2 在遇到 JSON 对象中存在重复名称时，会<strong>直接报错</strong>，而不是像 v1 那样默默接受最后一个。</li>
<li><strong>默认大小写敏感匹配：</strong> v2 的字段匹配将采用<strong>精确的、大小写敏感</strong>的方式。虽然也提供了 MatchCaseInsensitiveNames 选项和 nocase 标签来兼容特定场景，但“默认安全”的原则得到了贯彻。</li>
<li><strong>更强的未知键控制：</strong> v2 提供了 RejectUnknownMembers 选项（虽然非默认启用，但行为等同于 v1 的 DisallowUnknownFields），并引入了 unknown 标签，允许开发者将未知字段捕获到指定的 map 或 jsontext.Value 类型的字段中，而不是简单忽略。</li>
<li><strong>UnmarshalRead 校验 EOF：</strong> v2 的 UnmarshalRead 函数（用于处理 io.Reader）会校验整个输入流直到 EOF，从而有效阻止尾部垃圾数据的问题。</li>
<li><strong>更严格的 UTF-8 处理：</strong> v2 默认要求严格的 UTF-8 编码，对无效 UTF-8 会报错。</li>
</ul>
<p>这些改进，特别是<strong>默认行为的调整</strong>，将极大地提升 Go 应用在处理不可信 JSON 数据时的安全性，从源头上减少了许多潜在的漏洞。</p>
<h2>给 Go 开发者的关键启示</h2>
<p>在 JSONv2 真正成为主流之前，我们能做些什么来保护我们的 Go 应用呢？Trail of Bits 给出了一些宝贵的建议，结合 JSONv2 的趋势，我们可以总结为：</p>
<ol>
<li><strong>默认启用严格解析：</strong>
<ul>
<li>对于 encoding/json (v1)，尽可能使用 Decoder.DisallowUnknownFields() 来禁止未知字段。</li>
<li>警惕并正确使用 json:”-” 来忽略字段，避免误用 json:”-,omitempty” 或 json:”omitempty” 作为字段名。</li>
</ul>
</li>
<li><strong>保持服务边界的解析一致性：</strong> 当数据流经多个服务时（尤其是异构系统），确保所有环节对数据的解析行为（如重复键处理、大小写敏感性）是一致的。如果无法保证，需要在边界处增加额外的校验层。</li>
<li><strong>警惕数据格式混淆：</strong> 严格校验输入数据的 Content-Type，确保使用正确的解析器处理对应的数据格式。</li>
<li><strong>关注 JSONv2 的进展：</strong> 积极了解 JSONv2 的设计和特性，为未来可能的迁移做好准备，并理解其带来的安全增益。</li>
<li><strong>利用静态分析工具：</strong> Trail of Bits 提供了一些 Semgrep 规则来帮助检测代码库中常见的 JSON 解析误用模式。将静态分析集成到 CI/CD 流程中。</li>
<li><strong>编写明确的测试用例：</strong> 针对反序列化逻辑，编写包含各种边界情况（如重复键、不同大小写的键、未知键、垃圾数据）的测试用例，确保解析行为符合预期。</li>
</ol>
<h2>小结</h2>
<p>Trail of Bits 的这篇文章为我们所有 Go 开发者敲响了警钟：即使是像 encoding/json 这样基础、常用的标准库，也可能因为一些不符合直觉的默认行为或被忽视的配置，而成为安全攻击的突破口。</p>
<p>理解这些“隐秘角落”，认识到“便利”与“安全”之间的权衡，并积极拥抱像 JSONv2 这样的改进，是我们构建更健壮、更安全的 Go 应用的必经之路。在日常开发中，对任何外部输入都保持一份警惕，审慎处理数据的解析与校验，应成为我们每个人的习惯。</p>
<p><strong>你是否在项目中遇到过类似 Go 解析器的“坑”？你对 JSONv2 有哪些期待？欢迎在评论区分享你的经验和看法！</strong> 如果觉得本文对你有所启发，也请不吝点个【赞】和【在看】，让更多 Gopher 关注 Go 的解析器安全！</p>
<p>资料地址：https://blog.trailofbits.com/2025/06/17/unexpected-security-footguns-in-gos-parsers/</p>
<hr />
<p><strong>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</strong></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; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/06/22/unexpected-security-footguns-in-go-parsers/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go导出标识符：那些鲜为人知的细节</title>
		<link>https://tonybai.com/2025/01/23/the-hidden-details-of-go-exported-identifiers/</link>
		<comments>https://tonybai.com/2025/01/23/the-hidden-details-of-go-exported-identifiers/#comments</comments>
		<pubDate>Wed, 22 Jan 2025 19:24:06 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[embeded]]></category>
		<category><![CDATA[field]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.18]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[goplus]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[Method]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[Pointer]]></category>
		<category><![CDATA[struct]]></category>
		<category><![CDATA[structtag]]></category>
		<category><![CDATA[typeparameter]]></category>
		<category><![CDATA[Unicode]]></category>
		<category><![CDATA[UTF8]]></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=4471</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/01/23/the-hidden-details-of-go-exported-identifiers 前不久，在“Go+用户组”微信群里看到有开发者向七牛云老板许式伟反馈七牛云Go SDK中的某些类型没有导出，导致外部包无法使用的问题(如下图)： 七牛开发人员迅速对该问题做出了“更正”，将问题反馈中涉及的类型saveasArgs和saveasReply改为了导出类型，即首字母大写： 不过，这看似寻常的问题反馈与修正却引发了我的一些思考。 我们大胆臆想一下：如果saveasReply类型的开发者是故意将saveasReply类型设置为非导出的呢？看一下“更正”之前的saveasReply代码： type saveasReply struct { Fname string `json:"fname"` PersistenId string `json:"persistentId,omitempty"` Bucket string `json:"bucket"` Duration int `json:"duration"` // ms } 有读者可能会问：那为什么还将saveasReply结构体的字段设置为导出字段呢？请注意每个字段后面的结构体标签(struct tag)。这显然是为了进行JSON 编解码，因为目前Go的encoding/json包仅会对导出字段进行编解码处理。 除了这个原因，原开发者可能还希望包的使用者能够访问这些导出字段，而又不想完全暴露该类型。我在此不对这种设计的合理性进行评价，而是想探讨这种做法是否可行。 我们对Go导出标识符的传统理解是：导出标识符（以大写字母开头的标识符）可以在包外被访问和使用，而非导出标识符（以小写字母开头的标识符）只能在定义它们的包内访问。这种机制帮助开发者控制类型和函数的可见性，确保内部实现细节不会被随意访问，从而增强封装性。 但实际上，Go的导出标识符机制是否允许在某些情况下，即使类型本身是非导出的，其导出字段依然可以被包外的代码访问呢？该类型的导出方法呢？这些关于Go导出标识符的细节可能是鲜少人探讨的，在这篇博文中，我们将系统地了解这些机制，希望能为各位小伙伴带来更深入的理解。 1. Go对导出标识符的定义 我们先回顾一下Go语言规范(go spec)对导出标识符的定义： 我们通常使用英文字母来命名标识符，因此可以将上述定义中的第一句理解为：以大写英文字母开头的标识符即为导出标识符。 注：Unicode字符类别Lu（Uppercase Letter）包含所有的大写字母。这一类别不仅包括英文大写字母，还涵盖多种语言的大写字符，例如希腊字母、阿拉伯字母、希伯来字母和西里尔字母等。然而，我非常不建议大家使用非英文大写字母来表示导出标识符，因为这可能会挑战大家的认知习惯。 而第二句后半部分的描述往往被我们忽视或理解不够到位。一个类型的字段名和方法名可以是导出的，但并没有明确要求其关联的类型本身也必须是导出的。 这为我们提供了进一步探索Go导出标识符细节的机会。接下来，我们就用具体示例看看是否可以在包外访问非导出类型的导出字段以及导出方法。 2. 在包外访问非导出类型的导出字段 我们首先定义一个带有导出字段的非导出类型myStruct，并将它放在mypackage里： // go-exported-identifiers/field/mypackage/mypackage.go package mypackage type myStruct struct { Field string [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/the-hidden-details-of-go-exported-identifiers-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/01/23/the-hidden-details-of-go-exported-identifiers">本文永久链接</a> &#8211; https://tonybai.com/2025/01/23/the-hidden-details-of-go-exported-identifiers</p>
<p>前不久，在“Go+用户组”微信群里看到有开发者向七牛云老板许式伟反馈<a href="https://github.com/qiniu/go-sdk/blob/bb391c9d9ea2c115494df5c38d058cb3b673a29f/qvs/record.go#L41">七牛云Go SDK中的某些类型没有导出，导致外部包无法使用的问题(如下图)</a>：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-hidden-details-of-go-exported-identifiers-2.png" alt="" /></p>
<p>七牛开发人员迅速对该问题做出了“更正”，将问题反馈中涉及的类型saveasArgs和saveasReply改为了导出类型，即首字母大写：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-hidden-details-of-go-exported-identifiers-4.png" alt="" /></p>
<p>不过，这看似寻常的问题反馈与修正却引发了我的一些思考。</p>
<p>我们大胆臆想一下：如果saveasReply类型的开发者是故意将saveasReply类型设置为非导出的呢？看一下“更正”之前的saveasReply代码：</p>
<pre><code>type saveasReply struct {
    Fname       string `json:"fname"`
    PersistenId string `json:"persistentId,omitempty"`
    Bucket      string `json:"bucket"`
    Duration    int    `json:"duration"` // ms
}
</code></pre>
<p>有读者可能会问：那为什么还将saveasReply结构体的字段设置为导出字段呢？请注意每个字段后面的结构体标签(struct tag)。这显然是为了进行JSON 编解码，因为目前Go的encoding/json包仅会对导出字段进行编解码处理。</p>
<p>除了这个原因，原开发者可能还希望包的使用者能够访问这些导出字段，而又不想完全暴露该类型。我在此不对这种设计的合理性进行评价，而是想探讨这种做法是否可行。</p>
<p>我们对Go导出标识符的传统理解是：导出标识符（以大写字母开头的标识符）可以在包外被访问和使用，而非导出标识符（以小写字母开头的标识符）只能在定义它们的包内访问。这种机制帮助开发者控制类型和函数的可见性，确保内部实现细节不会被随意访问，从而增强封装性。</p>
<p>但实际上，Go的导出标识符机制是否允许在某些情况下，即使类型本身是非导出的，其导出字段依然可以被包外的代码访问呢？该类型的导出方法呢？这些关于Go导出标识符的细节可能是鲜少人探讨的，在这篇博文中，我们将系统地了解这些机制，希望能为各位小伙伴带来更深入的理解。</p>
<h2>1. Go对导出标识符的定义</h2>
<p>我们先回顾一下<a href="https://go.dev/ref/spec#Exported_identifiers">Go语言规范(go spec)对导出标识符的定义</a>：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-hidden-details-of-go-exported-identifiers-3.png" alt="" /></p>
<p>我们通常使用英文字母来命名标识符，因此可以将上述定义中的第一句理解为：以大写英文字母开头的标识符即为导出标识符。</p>
<blockquote>
<p>注：Unicode字符类别Lu（Uppercase Letter）包含所有的大写字母。这一类别不仅包括英文大写字母，还涵盖多种语言的大写字符，例如希腊字母、阿拉伯字母、希伯来字母和西里尔字母等。然而，我非常<strong>不建议大家使用非英文大写字母来表示导出标识符</strong>，因为这可能会挑战大家的认知习惯。</p>
</blockquote>
<p>而第二句后半部分的描述往往被我们忽视或理解不够到位。一个类型的字段名和方法名可以是导出的，但<strong>并没有明确要求其关联的类型本身也必须是导出的</strong>。</p>
<p>这为我们提供了进一步探索Go导出标识符细节的机会。接下来，我们就用具体示例看看是否可以在包外访问非导出类型的导出字段以及导出方法。</p>
<h2>2. 在包外访问非导出类型的导出字段</h2>
<p>我们首先定义一个带有导出字段的非导出类型myStruct，并将它放在mypackage里：</p>
<pre><code>// go-exported-identifiers/field/mypackage/mypackage.go

package mypackage

type myStruct struct {
    Field string // 导出的字段
}

// NewMyStruct1是一个导出的函数，返回myStruct的指针
func NewMyStruct1(value string) *myStruct {
    return &amp;myStruct{Field: value}
}

// NewMyStruct1是一个导出的函数，返回myStruct类型变量
func NewMyStruct2(value string) myStruct {
    return myStruct{Field: value}
}
</code></pre>
<p>然后我们在包外尝试访问myStruct类型的导出字段：</p>
<pre><code>// go-exported-identifiers/field/main.go

package main

import (
    "demo/mypackage"
    "fmt"
)

func main() {
    // 通过导出的函数获取myStruct的指针
    ms1 := mypackage.NewMyStruct1("Hello1")

    // 尝试访问Field字段
    fmt.Println(ms1.Field) // Hello1

    // 通过导出的函数获取myStruct类型变量
    ms2 := mypackage.NewMyStruct1("Hello2")

    // 尝试访问Field字段
    fmt.Println(ms2.Field) // Hello2
}
</code></pre>
<p>在go-exported-identifiers/field目录下编译运行该示例：</p>
<pre><code>$go run main.go
Hello1
Hello2
</code></pre>
<p>我们看到，无论是通过myStruct的指针还是实例副本，都可以成功访问其导出变量Field。这个示例的关键就是：我们<strong>使用了短变量声明</strong>直接通过调用myStruct的两个“构造函数(NewXXX)”得到了其指针(ms1)以及实例副本(ms2)。在这个过程中，我们没有在main包中显式使用mypackage.myStruct这个非导出类型。</p>
<p>采用类似的方案，我们接下来再看看是否可以在包外访问非导出类型的导出方法。</p>
<h2>3. 在包外访问非导出类型的导出方法</h2>
<p>我们为非导出类型添加两个导出方法M1和M2：</p>
<pre><code>// go-exported-identifiers/method/mypackage/mypackage.go

package mypackage

import "fmt"

type myStruct struct {
    Field string // 导出的字段
}

// NewMyStruct1是一个导出的函数，返回myStruct的指针
func NewMyStruct1(value string) *myStruct {
    return &amp;myStruct{Field: value}
}

// NewMyStruct1是一个导出的函数，返回myStruct类型变量
func NewMyStruct2(value string) myStruct {
    return myStruct{Field: value}
}

func (m *myStruct) M1() {
    fmt.Println("invoke *myStruct's M1")
}

func (m myStruct) M2() {
    fmt.Println("invoke myStruct's M2")
}
</code></pre>
<p>然后，试着在外部包中调用M1和M2方法：</p>
<pre><code>// go-exported-identifiers/method/main.go

package main

import (
    "demo/mypackage"
)

func main() {
    // 通过导出的函数获取myStruct的指针
    ms1 := mypackage.NewMyStruct1("Hello1")
    ms1.M1()
    ms1.M2()

    // 通过导出的函数获取myStruct类型变量
    ms2 := mypackage.NewMyStruct2("Hello2")
    ms2.M1()
    ms2.M2()
}
</code></pre>
<p>在go-exported-identifiers/method目录下编译运行这个示例：</p>
<pre><code>$go run main.go
invoke *myStruct's M1
invoke myStruct's M2
invoke *myStruct's M1
invoke myStruct's M2
</code></pre>
<p>我们看到，无论是通过非导出类型的指针，还是通过非导出类型的变量复本都可以成功调用非导出类型的导出方法。</p>
<p>提及方法，我们会顺带想到接口，非导出类型是否可以实现某个外部包定义的接口呢？我们继续往下看。</p>
<h2>4. 非导出类型实现某个外部包的接口</h2>
<p>在Go中，如果某个类型T实现了某个接口类型I的方法集合中的所有方法，我们就说T实现了I，T的实例可以赋值给I类型的接口变量。</p>
<p>在下面示例中，我们看看非导出类型是否可以实现某个外部包的接口。</p>
<p>在这个示例中mypackage包中的内容与上面示例一致，主要改动的是main.go，我们来看一下：</p>
<pre><code>// go-exported-identifiers/interface/main.go

package main

import (
    "demo/mypackage"
)

// 定义一个导出的接口
type MyInterface interface {
    M1()
    M2()
}

func main() {
    var mi MyInterface

    // 通过导出的函数获取myStruct的指针
    ms1 := mypackage.NewMyStruct1("Hello1")
    mi = ms1
    mi.M1()
    mi.M2()

    // 通过导出的函数获取myStruct类型变量
    // ms2 := mypackage.NewMyStruct2("Hello2")
    // mi = ms2 // compile error: mypackage.myStruct does not implement MyInterface
    // ms2.M1()
    // ms2.M2()
}
</code></pre>
<p>在这个main.go中，我们定义了一个接口MyInterface，它的方法集合中有两个方法M1和M2。根据类型方法集合的判定规则，&#42;myStruct类型实现了MyInterface的所有方法，而myStruct类型则不满足，没有实现M1方法，我们在go-exported-identifiers/interface目录下编译运行这个示例，看看是否与我们预期的一致：</p>
<pre><code>$go run main.go
invoke *myStruct's M1
invoke myStruct's M2
</code></pre>
<p>如果我们去掉上面代码中对ms2的注释，那么将得到Compiler error: mypackage.myStruct does not implement MyInterface。</p>
<blockquote>
<p>注：关于一个类型的方法集合的判定规则，可以参考我的极客时间<a href="http://gk.link/a/10AVZ">《Go语言第一课》</a>专栏的<a href="https://time.geekbang.org/column/article/466221">第25讲</a>。</p>
</blockquote>
<p>接下来，我们再来考虑一个场景，即非导出类型用作嵌入字段的情况，我们要看看该非导出类型的导出方法和导出字段是否会promote到外部类型中。</p>
<h2>5. 非导出类型用作嵌入字段</h2>
<p>我们改造一下示例，新版的带有嵌入字段的结构见下面mypackage包的代码：</p>
<pre><code>// go-exported-identifiers/embedded_field/mypackage/mypackage.go

package mypackage

import "fmt"

type nonExported struct {
    Field string // 导出的字段
}

// Exported 是导出的结构体，嵌入了nonExported
type Exported struct {
    nonExported // 嵌入非导出结构体
}

func NewExported(value string) *Exported {
    return &amp;Exported{
        nonExported: nonExported{
            Field: value,
        },
    }
}

// M1是导出的函数
func (n *nonExported) M1() {
    fmt.Println("invoke nonExported's M1")
}

// M2是导出的函数
func (e *Exported) M2() {
    fmt.Println("invoke Exported's M2")
}
</code></pre>
<p>这里新增一个导出类型Exported，它嵌入了一个非导出类型nonExported，后者拥有导出字段Field，以及两个导出方法M1。我们也Exported类型定义了一个方法M2。</p>
<p>下面我们再来看看main.go中是如何使用Exported的：</p>
<pre><code>// go-exported-identifiers/embedded_field/main.go

package main

import (
    "demo/mypackage"
    "fmt"
)

// 定义一个导出的接口
type MyInterface interface {
    M1()
    M2()
}

func main() {
    ms := mypackage.NewExported("Hello")
    fmt.Println(ms.Field) // 访问嵌入的非导出结构体的导出字段

    ms.M1() // 访问嵌入的非导出结构体的导出方法

    var mi MyInterface = ms
    mi.M1()
    mi.M2()
}
</code></pre>
<p>在go-exported-identifiers/embedded_field目录下编译运行这个示例：</p>
<pre><code>$go run main.go
Hello
invoke nonExported's M1
invoke nonExported's M1
invoke Exported's M2
</code></pre>
<p>我们看到，作为嵌入字段的非导出类型的导出字段与方法会被自动promote到外部类型中，通过外部类型的变量可以直接访问这些字段以及调用这些导出方法。这些方法还可以作为外部类型方法集中的一员，来作为满足特定接口类型(如上面代码中的MyInterface)的条件。</p>
<p>Go 1.18增加了泛型支持，那么非导出类型是否可以用作泛型函数和泛型类型的类型实参呢？最后我们来看看这个细节。</p>
<h2>6. 非导出类型用作泛型函数和泛型类型的类型实参</h2>
<p>和前面一样，我们先定义用于该示例的带有导出字段和导出方法的非导出类型：</p>
<pre><code>// go-exported-identifiers/generics/mypackage/mypackage.go

package mypackage

import "fmt"

// 定义一个非导出的结构体
type nonExported struct {
    Field string
}

// 导出的方法
func (n *nonExported) M1() {
    fmt.Println("invoke nonExported's M1")
}

func (n *nonExported) M2() {
    fmt.Println("invoke nonExported's M2")
}

// 导出的函数，用于创建非导出类型的实例
func NewNonExported(value string) *nonExported {
    return &amp;nonExported{Field: value}
}
</code></pre>
<p>现在我们将其用于泛型函数，下面定义了泛型函数UseNonExportedAsTypeArgument，它的类型参数使用MyInterface作为约束，而上面的nonExported显然满足该约束，我们通过构造函数NewNonExported获得非导出类型的实例，然后将其传递给UseNonExportedAsTypeArgument，Go会通过泛型的类型参数自动推导机制推断出类型实参的类型：</p>
<pre><code>// go-exported-identifiers/generics/main.go

package main

import (
    "demo/mypackage"
)

// 定义一个用作约束的接口
type MyInterface interface {
    M1()
    M2()
}

func UseNonExportedAsTypeArgument[T MyInterface](item T) {
    item.M1()
    item.M2()
}

// 定义一个带有泛型参数的新类型
type GenericType[T MyInterface] struct {
    Item T
}

func NewGenericType[T MyInterface](item T) GenericType[T] {
    return GenericType[T]{Item: item}
}

func main() {
    // 创建非导出类型的实例
    n := mypackage.NewNonExported("Hello")

    // 调用泛型函数，传入实现了MyInterface的非导出类型
    UseNonExportedAsTypeArgument(n) // ok

    // g := GenericType{Item: n} // compiler error: cannot use generic type GenericType[T MyInterface] without instantiation
    g := NewGenericType(n)
    g.Item.M1()
}
</code></pre>
<p>但由于目前Go泛型还不支持对泛型类型的类型参数的自动推导，所以直接通过g := GenericType{Item: n}来初始化一个泛型类型变量将导致编译错误！我们需要借助泛型函数的推导机制将非导出类型与泛型类型进行结合，参见上述示例中的NewGenericType函数，通过泛型函数支持的类型参数的自动推导间接获得GenericType的类型实参。在go-exported-identifiers/generics目录下编译运行这个示例，便可得到我们预期的结果：</p>
<pre><code>$go run main.go
invoke nonExported's M1
invoke nonExported's M2
invoke nonExported's M1
</code></pre>
<h2>7. 非导出类型使用导出字段以及导出方法的用途</h2>
<p>前面的诸多示例证明了：即使类型本身是非导出的，但其内部的导出字段以及它的导出方法依然可以在外部包中使用，并且在实现接口、嵌入字段、泛型等使用场景下均有效。</p>
<p>到这里，你可能会提出这样一个问题：<strong>会有Go开发者使用非导出类型结合导出字段或方法的设计吗</strong>？</p>
<p>其实这种还是很常见的，在Go标准库中就有不少，只不过它们更多是包内使用，类似于非导出类型xxxImpl和它的Wrapper类型XXX的关系，或是xxxImpl或嵌入到XXX中，就像这样：</p>
<pre><code>// 包内实现
type xxxImpl struct {  // 非导出的实现类型
    // 内部字段
}

// 导出的包装类型
type XXX struct {
    impl *xxxImpl  // 包含实现类型
    // 其他字段
}

// 或者通过嵌入方式
type XXX struct {
    *xxxImpl  // 嵌入实现类型
    // 其他字段
}
</code></pre>
<p>但也有一些可以包外使用的，比如实现了某个接口，并通过接口值返回，提供给外部使用，例如下面的valueCtx，它实现了Context接口，并通过WithValue返回，供调用WithValue的外部包使用：</p>
<pre><code>//$GOROOT/src/context/context.go

func WithValue(parent Context, key, val any) Context {  // 构造函数，实现接口
    if parent == nil {
        panic("cannot create context from nil parent")
    }
    if key == nil {
        panic("nil key")
    }
    if !reflectlite.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &amp;valueCtx{parent, key, val}
}

// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
    Context
    key, val any
}

func (c *valueCtx) Value(key any) any {
    if c.key == key {
        return c.val
    }
    return value(c.Context, key)
}
</code></pre>
<p>这么做的目的是什么呢？大约有如下几点：</p>
<ul>
<li>隐藏实现细节</li>
</ul>
<p>非导出类型的主要作用是防止外部直接使用和依赖其内部实现细节。通过限制类型的直接使用，库作者可以保持实现的灵活性，随时调整或重构类型的内部逻辑，而无需担心破坏外部调用代码； 还可以避免暴露多余的API，使库的接口更加简洁。</p>
<ul>
<li>控制实例的创建和管理</li>
</ul>
<p>通过非导出类型，开发者还可以确保外部代码无法直接实例化类型，而必须通过导出的构造函数或工厂函数，就像前面举的示例那样。这种模式可以保证对象始终以特定的方式初始化，避免错误使用。同时，它还可以用来实现更复杂的初始化逻辑，如依赖注入或资源管理。</p>
<ul>
<li>在接口实现中的作用</li>
</ul>
<p>非导出类型可以用来实现导出的接口，从而将接口的实现细节完全隐藏。对于用户来说，只需要关心接口的定义，而无需关注其实现。</p>
<h2>8. 小结</h2>
<p>本文探讨了Go语言中的导出标识符及其相关细节，特别是非导出类型如何与其导出字段和导出方法结合使用。</p>
<p>尽管某些类型是非导出的，其内部的导出字段和方法依然可以在包外访问。此外，非导出类型在实现接口、嵌入字段和泛型中也展现出良好的应用。这种设计不仅促进了封装和接口实现的灵活性，还允许开发者通过构造函数返回非导出类型的实例，从而有效控制实例的创建与管理。这种方式帮助隐藏实现细节，简化外部接口，使得代码结构更加清晰。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/go-exported-identifiers">这里</a>下载。</p>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾<br />
。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/01/23/the-hidden-details-of-go-exported-identifiers/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go字符串比较，终于有人讲清楚了</title>
		<link>https://tonybai.com/2022/04/18/inside-go-string-comparison/</link>
		<comments>https://tonybai.com/2022/04/18/inside-go-string-comparison/#comments</comments>
		<pubDate>Sun, 17 Apr 2022 23:24:46 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[ASCII]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[codepoint]]></category>
		<category><![CDATA[Compare]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Go语言第一课]]></category>
		<category><![CDATA[Go语言精进之路]]></category>
		<category><![CDATA[String]]></category>
		<category><![CDATA[Unicode]]></category>
		<category><![CDATA[UTF8]]></category>
		<category><![CDATA[字符串]]></category>
		<category><![CDATA[极客时间]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[比较]]></category>
		<category><![CDATA[码点]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3501</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/04/18/inside-go-string-comparison 西娅(Thea)是一个刚刚入门Go语言的妹子程序员，今天她遇到了一个让她“surprise”的问题。下面就是那段让妹子西娅困惑的Go代码： func main() { s1 := "12345" s2 := "2" fmt.Println(`"12345" &#62; "2":`, s1 &#62; s2) // false s3 := "零" s4 := "一" s5 := "二" fmt.Println(`"一" &#62; "零":`, s4 &#62; s3) // false fmt.Println(`"二" &#62; "零":`, s5 &#62; s3) // false fmt.Println(`"二" &#62; "一":`, s5 &#62; s4) // true } [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/inside-go-string-comparison-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/04/18/inside-go-string-comparison">本文永久链接</a> &#8211; https://tonybai.com/2022/04/18/inside-go-string-comparison</p>
<p>西娅(Thea)是一个刚刚入门Go语言的妹子程序员，今天她遇到了一个让她“surprise”的问题。下面就是那段让妹子西娅困惑的Go代码：</p>
<pre><code>func main() {
    s1 := "12345"
    s2 := "2"
    fmt.Println(`"12345" &gt; "2":`, s1 &gt; s2) // false

    s3 := "零"
    s4 := "一"
    s5 := "二"

    fmt.Println(`"一" &gt; "零":`, s4 &gt; s3) // false
    fmt.Println(`"二" &gt; "零":`, s5 &gt; s3) // false
    fmt.Println(`"二" &gt; "一":`, s5 &gt; s4) // true
}
</code></pre>
<p>在这段关于Go字符串比较的代码中：</p>
<ul>
<li>为什么表达式”12345&#8243; > “2&#8243;的求值结果是false呢？</li>
<li>为什么”一” > “零”和”二” > “零”两个表达式的求值结果都是false呢？</li>
<li>而”二” > “一”的求值结果却又为true呢？</li>
</ul>
<p>四个结果都让西娅百思不得其解！于是西娅在网络上寻找能为其解惑的Go技术资料。</p>
<p>她网上看到一本名为《Go语言精进之路》的“小黄书”，据说这本书中有有关Go字符串原理与字符串比较的详细讲解。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-programming-from-beginner-to-master-qr.png" alt="" /></p>
<p>西娅不经意间瞥见，旁边的同事Tony桌上摆着一本黄色的、厚重的书，这不正是她想看的吗！于是西娅向Tony发出了借书一阅的请求。Tony面对“美女攻势”向来是“每战必败”的，于是西娅顺利地拿到了两卷本的《Go语言精进之路》。借午休时间，西娅花了1.5个小时认真学习了书中有关Go字符串的三个章节：第15节的“了解string实现原理和高效使用”、 第52节的“掌握字符集的原理和字符编码方案间的转换”和第56节的“掌握bytes包和strings包的基本操作”。看完后大呼Wonderful！书中的讲解完全解答了西娅的问题。</p>
<p>此时西娅想起了在<a href="http://gk.link/a/10AVZ">《Go语言第一课专栏》</a>的结课语<a href="https://time.geekbang.org/column/article/486536">《和你一起迎接Go的黄金十年》</a>中作者关于学习Go语言方法的建议：<strong>输出</strong>大法！通过输出将学到的知识真正内化为自己的知识，于是西娅将自己对书中内容的理解记录了下来。恰好此时旁边的Tony刚刚从午睡中苏醒过来，西娅决定再为一把人师。Tony就这样被稀里糊涂地拽了过来充当学生:)。</p>
<p>以下是西娅的讲解。</p>
<hr />
<h3>1. Go语言中的字符串类型</h3>
<p>字符串类型是现代编程语言中最常使用的数据类型之一。在Go语言的先祖之一C语言当中，字符串类型并没有被显式定义，而是以字符串字面值<br />
常量或以&#8217;\0&#8242;结尾的字符类型（char）数组来呈现的。</p>
<p>Go语言修复了C语言的这一“缺陷”，原生内置了string类型，统一了对“字符串”的抽象。在Go语言中，无论是字符串常量、字符串变量或是代码中出现的字符串字面量，它们的类型都被统一设置为<strong>string</strong>。</p>
<p>Go的string类型设计充分吸取了C语言字符串设计的经验教训，并结合了其他主流语言在字符串类型设计上的最佳实践，最终为Gopher呈现的string类型具有如下功能特点：</p>
<ul>
<li>string类型的数据是不可变的</li>
</ul>
<p>即一旦声明了一个string类型的标识符，无论是常量还是变量，该标识符所指代的数据在整个程序的生命周期内便无法被更改。</p>
<ul>
<li>零值可用</li>
</ul>
<p>Go string类型支持零值可用的理念。Go字符串无需像C语言中那样考虑结尾&#8217;\0&#8242;字符，因此其零值为”"，长度为0。</p>
<ul>
<li>
<p>获取长度的时间复杂度是O(1)级别</p>
</li>
<li>
<p>支持各种比较关系操作符：==、!= 、>=、&lt;=、> 和&lt;</p>
</li>
</ul>
<p>鉴于Go string是不可变的，因此如果两个字符串的长度不相同，那么无需比较具体字符串数据，也可以断定两个字符串是不同的；如果长度相<br />
同，则要进一步判断数据指针是否指向同一块底层存储数据。如果相同，则两个字符串是等价的，如果不同，则还需进一步去比对实际的数据内容。至于怎么比较，我接下来会讲。</p>
<ul>
<li>对非ASCII字符提供原生支持</li>
</ul>
<p>这一特点就涉及到Go字符串中的字符是什么字符、用什么字符编码的问题了。下面我们就来看看。</p>
<h3>2. Go字符串采用的字符集编码</h3>
<p>Go语言默认使用Unicode字符集，并采用UTF-8编码方案，Go还提供了rune原生类型来表示Unicode字符。Unicode（万国码/统一码）在1994年发布，它是以收纳人类所有字符为目的的统一字符集。Unicode字符集就是将世界上存在的绝大多数常用字符进行统一排队和编号。比如下面是一个Unicode字符集表的片段：</p>
<table>
<thead>
<tr>
<th align="center">序号</th>
<th align="center">字符</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">U+0000</td>
<td align="center">&#8230; &#8230;</td>
</tr>
<tr>
<td align="center">&#8230; &#8230;</td>
<td align="center">&#8230; &#8230;</td>
</tr>
<tr>
<td align="center">U+0031</td>
<td align="center">1</td>
</tr>
<tr>
<td align="center">U+0032</td>
<td align="center">2</td>
</tr>
<tr>
<td align="center">&#8230; &#8230;</td>
<td align="center">&#8230; &#8230;</td>
</tr>
<tr>
<td align="center">U+4E2D</td>
<td align="center">中</td>
</tr>
<tr>
<td align="center">&#8230; &#8230;</td>
<td align="center">&#8230; &#8230;</td>
</tr>
<tr>
<td align="center">U+4EBA</td>
<td align="center">人</td>
</tr>
<tr>
<td align="center">&#8230; &#8230;</td>
<td align="center">&#8230; &#8230;</td>
</tr>
<tr>
<td align="center">U+56FD</td>
<td align="center">国</td>
</tr>
<tr>
<td align="center">&#8230; &#8230;</td>
<td align="center">&#8230; &#8230;</td>
</tr>
<tr>
<td align="center">U+10FFFF</td>
<td align="center">&#8230; &#8230;</td>
</tr>
</tbody>
</table>
<p>我们看到每个Unicode字符(比如表格里的”1&#8243;、”中”等)都有自己的唯一序号，这个序号就叫做字符的码点(code point)，Go中的rune类型可用于表示码点。</p>
<p>好了，问题来了！Unicode字符集表格有了，Go是如何在内存中存储这些字符的呢？目前业界有多种存储方案，比如：UTF-32(即4个字节表示每个Unicode字符码点）、UTF-16(使用2个字节或4个字节表示每个Unicode字符码点)以及UTF-8。</p>
<p>UTF-8使用变长度字节对Unicode字符（的码点）进行编码。编码采用的字节数量与Unicode字符在码点表中的序号有关：<strong>表示序号（码点）小的字符使用的字节数量就少，表示序号（码点）大的字符使用的字节数量就多</strong>。</p>
<p>UTF-8编码使用的字节数量从1个到4个不等。前128个与ASCII字符重合的码点（U+0000~U+007F）使用1个字节表示；带变音符号的拉丁文、希腊文、西里尔字母、阿拉伯文等使用2个字节来表示；而东亚文字（包括汉字）使用3个字节表示；其他极少使用的语言的字符则使用4个字节表示。</p>
<p>这样的编码方案是兼容ASCII字符内存表示的，这意味着采用UTF-8方案在内存中表示Unicode字符时，已有的ASCII字符可以被直接当成Unicode字符进行存储和传输，无需做任何改变。相对于UTF-16和UTF-32方案，UTF-8方案的空间利用率也是最高的。并且，utf8解码和编码时，也无需考虑字节序问题。</p>
<p>于是，Go语言使用了Utf8编码方案在内存中存储Unicode字符。</p>
<p>以字符“中”为例，它的码点(序号)为U+4E2D，它在Utf8编码则为“0xE4 0xB8 0xAD”，即在内存中Go实际用三个字节来表示“中”这个Unicode字符。</p>
<h3>3. Go字符串比较</h3>
<p>上面铺垫了这么些内容，就是为了为字符串比较开道。关于Go字符串比较，<a href="https://go.dev/ref/spec">Go语言规范</a>中只说了一句话：<strong>String values are comparable and ordered, lexically byte-wise</strong>。什么意思呢？这句话表达了三个意思：</p>
<ul>
<li>定性：字符串可比较</li>
<li>定量：字符串是有序的</li>
<li>方法：逐字节</li>
</ul>
<p>下面我对开篇的例子做逐一说明，首先看下面代码：</p>
<pre><code>s1 := "12345"
s2 := "2"
fmt.Println(`"12345" &gt; "2":`, s1 &gt; s2)
</code></pre>
<p>s1和s2两个字符串中的字符都是ASCII字符范畴的，每个字符在内存中的编码都是一个字节。按照Go string比较的原理，我们对s1和s2进行逐字节比较。首先比较s1的第一个字符”1&#8243;和s2的第一个字符”2&#8243;。字符”2&#8243;在内存中的字节为0&#215;32，而字符”1&#8243;在内存中的字节为0&#215;31，显然0&#215;32大于0&#215;31，到这里已经比出大小了，程序不会继续对后续的字符进行比对了。这也是为什么s1 > s2这个表达式为false的原因。</p>
<p>如果s2 = “12346&#8243;呢？那么按照Go string比较的原理，程序在比较s1和s2的前4个字符时都相等，于是只能由第5个字符来判定两个字符串的大小了，s2的第五个字符”6&#8243;显然大于s1的第五个字符”5&#8243;，于是当s2=”12346&#8243;时，s2是大于s1的。</p>
<p>我们再看看含有汉字的字符串的例子：</p>
<pre><code>s3 := "零"
s4 := "一"
s5 := "二"

fmt.Println(`"一" &gt; "零":`, s4 &gt; s3) // false
fmt.Println(`"二" &gt; "零":`, s5 &gt; s3) // false
fmt.Println(`"二" &gt; "一":`, s5 &gt; s4) // true
</code></pre>
<p>为了方便后续说明，我们先把”零”、”一”和”二”这三个汉字的Utf8编码计算出来：</p>
<ul>
<li>“零”的UTF8编码为：0xE9 0x9B 0xB6</li>
<li>“一”的UTF8编码为：0xE4 0xB8 0&#215;80</li>
<li>“二”的UTF8编码为：0xE4 0xBA 0x8C</li>
</ul>
<p>我们看到，三个汉字的Utf8编码都是三个字节。</p>
<p>好了接下来，我们先比较s4(“一”)和s3(“零”)。根据Go字符串比较原理，程序对s3和s4做逐字节比较，”零”这个字符的第一个字节为0xE9，而”一”这个字符的第一个字节为0xE4，我们知道0xE9 > 0xE4，于是比较停止，判定：s3 > s4。</p>
<p>同理，s3 > s5。</p>
<p>在比较s4(“一”)和s5(“二”)时，由于它们的第一个字节都是0xE4，于是第二个字节决定了它们的大小，0xBA > 0xB8，所以s5 > s4。</p>
<h3>4. Go strings包中的Compare函数</h3>
<p>Go标准库在strings包中提供了Compare函数用于对两个字符串做大小比较。但按照Go团队的comment，这个函数存在的意义更多是是为了与bytes包尽量保持API的一致，其自身也是使用原生排序比较操作符实现的：</p>
<pre><code>// $GOROOT/src/strings/compare.go
func Compare(a, b string) int {
    if a == b {
        return 0
    }
    if a &lt; b {
        return -1
    }
    return +1
}
</code></pre>
<p>实际应用中，我们很少使用strings.Compare更多的是直接使用<strong>排序比较操作符</strong>对字符串类型变量进行比较，这样更直观，性能大多数场景也会更高，毕竟少一次函数调用。</p>
<hr />
<p>“好了以上就是我要讲给你听的，听懂了么”。西娅兴高采烈地对此时已经处于清醒状态的Tony说。</p>
<p>“讲的真好。比我书里讲的还透彻”。Tony一边鼓掌一边微笑着说。“程序员妹子西娅Thea终于把Go字符串比较讲清楚了”。</p>
<p>西娅惊讶！“你的什么书”？</p>
<p>Tony指了指办公桌上的小黄书说：“这书就是我写的啊^_^”。</p>
<p>西娅脸上现出一丝红晕&#8230; &#8230;。</p>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-k8s-practice-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>微信公众号：iamtonybai</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>“Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/04/18/inside-go-string-comparison/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go语言的“黑暗角落”：盘点学习Go语言时遇到的那些陷阱[译]（第二部分）</title>
		<link>https://tonybai.com/2021/03/29/darker-corners-of-go-part2/</link>
		<comments>https://tonybai.com/2021/03/29/darker-corners-of-go-part2/#comments</comments>
		<pubDate>Mon, 29 Mar 2021 09:24:22 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[byte]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Const]]></category>
		<category><![CDATA[defer]]></category>
		<category><![CDATA[embed]]></category>
		<category><![CDATA[for]]></category>
		<category><![CDATA[for-range]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[gofmt]]></category>
		<category><![CDATA[goformat]]></category>
		<category><![CDATA[goimports]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[golint]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[heap]]></category>
		<category><![CDATA[IDE]]></category>
		<category><![CDATA[import]]></category>
		<category><![CDATA[init]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[iota]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[Mutex]]></category>
		<category><![CDATA[nil]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[pitfall]]></category>
		<category><![CDATA[race]]></category>
		<category><![CDATA[Reflect]]></category>
		<category><![CDATA[rune]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[Stack]]></category>
		<category><![CDATA[String]]></category>
		<category><![CDATA[strings]]></category>
		<category><![CDATA[struct]]></category>
		<category><![CDATA[sync]]></category>
		<category><![CDATA[time]]></category>
		<category><![CDATA[trap]]></category>
		<category><![CDATA[Unicode]]></category>
		<category><![CDATA[unsafe]]></category>
		<category><![CDATA[utf-8]]></category>
		<category><![CDATA[UTF8]]></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>
		<category><![CDATA[类型嵌入]]></category>
		<category><![CDATA[陷阱]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3141</guid>
		<description><![CDATA[本文翻译自Rytis Bieliunas的文章《Darker Corners of Go》。 第一部分参见《Go语言的“黑暗角落”：盘点学习Go语言时遇到的那些陷阱[译]（第一部分）》 7. 字符串和字节数组 1) Go中的字符串 Go字符串的内部定义如下所示： type StringHeader struct { Data uintptr Len int } 字符串本身是一个值类型，它具有一个指向字节数组的指针和固定长度。字符串中的“零字节”不像在C中那样标记着字符串的结尾。字符串内可以有任何数据。通常，该数据被编码为UTF-8字符串，但不一定如此。 2) 字符串不能为nil 字符串在Go中永远不会为nil。字符串的默认值是一个空字符串，而不是nil： package main import "fmt" func main() { var s string fmt.Println(s == "") // true s = nil // error: cannot use nil as type string in assignment } 3) 字符串是不可变的（某种） [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/darker-corners-of-go-1.png" alt="" /></p>
<p>本文翻译自Rytis Bieliunas的文章<a href="https://rytisbiel.com/2021/03/06/darker-corners-of-go/">《Darker Corners of Go》</a>。</p>
<p>第一部分参见<a href="https://tonybai.com/2021/03/29/darker-corners-of-go-part1">《Go语言的“黑暗角落”：盘点学习Go语言时遇到的那些陷阱[译]（第一部分）》</a></p>
<h2>7. 字符串和字节数组</h2>
<h3>1) Go中的字符串</h3>
<p>Go字符串的内部定义如下所示：</p>
<pre><code>type StringHeader struct {
    Data uintptr
    Len  int
}
</code></pre>
<p>字符串本身是一个值类型，它具有一个指向字节数组的指针和固定长度。字符串中的“零字节”不像在C中那样标记着字符串的结尾。字符串内可以有任何数据。通常，该数据被编码为UTF-8字符串，但不一定如此。</p>
<h3>2) 字符串不能为nil</h3>
<p>字符串在Go中永远不会为nil。字符串的默认值是一个空字符串，而不是nil：</p>
<pre><code>package main

import "fmt"

func main() {
    var s string
    fmt.Println(s == "") // true

    s = nil // error: cannot use nil as type string in assignment
}
</code></pre>
<h3>3) 字符串是不可变的（某种）</h3>
<p>Go不想让您修改字符串：</p>
<pre><code>package main

func main() {
    str := "darkercorners"
    str[0] = 'D' // error: cannot assign to str[0]
}
</code></pre>
<p>不可变的数据更易于推理，因此产生的问题更少。缺点是每次您想在字符串中添加或删除某些内容时，都必须分配一个全新的字符串。如果确实需要，可以通过unsafe包来修改字符串，但是如果您这这样做的话，你可能就是聪明过头了。</p>
<p>您可能要担心分配的最常见情况是，需要将许多字符串连接在一起。有一个string.Builder类型用于此目的。strings.Builder批量分配内存，而不是每次添加字符串时分配内存：</p>
<pre><code>package main

import (
    "strconv"
    "strings"
    "testing"
)

func BenchmarkString(b *testing.B) {
    var str string
    for i := 0; i &lt; b.N; i++ {
        str += strconv.Itoa(i)
    }
}

func BenchmarkStringBuilder(b *testing.B) {
    var str strings.Builder
    for i := 0; i &lt; b.N; i++ {
        str.WriteString(strconv.Itoa(i))
    }
}
</code></pre>
<pre><code>BenchmarkString-8 401053 147346 ns/op 1108686 B/op 2 allocs/op
BenchmarkStringBuilder-8 29307392 44.9 ns/op 52 B/op 0 allocs/op
</code></pre>
<p>在此示例中，使用strings.Builder比简单添加字符串（并每次分配新的内存）快3000倍。</p>
<p>在某些情况下，Go编译器会优化这些分配：</p>
<ul>
<li>比较字符串和字节切片时：str == string(byteSlice)</li>
<li>当使用[]byte键在map[string]中查找条目时：m[string(byteSlice)]</li>
<li>在将字符串转换为字节的range子句中：对于i，v：= range []byte(str) {…}</li>
</ul>
<p>Go编译器的新版本可能会添加更多优化，因此，如果性能至关重要，那么最好始终使用基准测试和剖析器(profiler)。</p>
<h3>4) 字符串与[]byte</h3>
<p>修改字符串的一种方法是先将其转换为字节切片，然后再转换回字符串。如下例所示，将字符串转换为字节切片并向后复制整个字符串和字节切片。原始字符串不变：</p>
<pre><code>package main

import (
    "fmt"
)

func main() {
    str := "darkercorners"
    bytes := []byte(str)

    bytes[0] = 'D'

    str2 := string(bytes)

    bytes[6] = 'C'

    // prints: darkercorners Darkercorners DarkerCorners
    fmt.Println(str, str2, string(bytes))
}
</code></pre>
<p>使用unsafe包可以（但显然不安全）直接修改字符串而无需分配内存。</p>
<blockquote>
<p>导入unsafe包可能是不可移植的，并且不受Go 1兼容性准则的保护。 &#8211; https://golang.org/pkg/unsafe/</p>
</blockquote>
<pre><code>package main

import (
    "fmt"
    "unsafe"
)

func main() {
    buf := []byte("darkercorners")
    buf[0] = 'D'

    // make a string that points to the same data as buf byte slice
    str := *(*string)(unsafe.Pointer(&amp;buf))

    // modifying byte slice
    // it now points to the same memory as the string does
    // str is modified here as well
    buf[6] = 'C'

    fmt.Println(str, string(buf)) // DarkerCorners DarkerCorners
}
</code></pre>
<h3>5) UTF-8的那些事</h3>
<p>Unicode和UTF-8是一个“有故事”的主题。要了解Unicode和UTF-8的总体工作原理，您可能需要阅读Joel Spolsky的博客文章<a href="https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/">“每个软件开发人员的绝对最低限度，绝对，肯定地必须了解Unicode和字符集（无借口！）”</a>。</p>
<p>下面是简短回顾：</p>
<ul>
<li>Unicode是“一种用于不同语言和脚本的国际编码标准，通过该标准，为每个字母，数字或符号分配了适用于不同平台和程序的唯一数值”。本质上，这是一张“码点”的大表。它包含所有语言的大多数（但不是全部）字符。该表中的每个码点都有一个索引，您有时可以看到使用U+表示法指定的索引，例如字母A的U+0041。</li>
<li>通常，码点是指一个字符，例如汉字⻯（U+2EEF），但是它可以是几何形状或字符修饰符（例如，德语ä，ö和ü等字母的变音符号）。由于某种原因，它甚至可能是便便图标（U+1F4A9）。</li>
<li>UTF-8是将大Unicode表的元素编码为计算机可以使用的实际字节的一种方法（也是最常见的一种方法）。</li>
<li>使用UTF-8编码时，单个Unicode码点可能占用1到4个字节。</li>
<li>数字和拉丁字母（az，AZ，0-9）编码为1个字节。许多其他语言的字母将以UTF-8编码占用1个以上的字节。</li>
<li>如果您不了解上面这点，则一旦有人需要将其与其他语言一起使用时，您的Go程序可能会中断。除非您当然会仔细阅读本章的其余部分。</li>
</ul>
<h3>6) Go中的字符串编码</h3>
<p>Go中的字符串是字节数组。字符串本身对编码一无所知。它不必是UTF-8编码的。尽管某些库函数甚至是一种语言功能（for range循环）都假设它是utf-8编码的。</p>
<p>相信Go字符串都是UTF-8并不少见。字符串字面量(literals)使这一混乱增加了很多。尽管字符串本身没有任何特定的编码，但是Go编译器始终将源代码解释为UTF-8。</p>
<p>定义字符串字面量后，您的编辑器会将其与其余代码一样保存为UTF-8编码的Unicode字符串。一旦Go解析了程序，它将被编译到您的程序中。编译器或Go字符串处理代码与最终编码为UTF-8的字符串无关-这只是文本编辑器将字符串写入磁盘的方式：</p>
<pre><code>package main

import (
    "fmt"
)

func main() {
    // a string literal with Unicode characters
    s := "English 한국어"

    // prints the expected Unicode string: English 한국어
    fmt.Println(s)
}

</code></pre>
<p>只是为了证明一点，这是定义非UTF-8字符串的方法：</p>
<pre><code>package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    s := "\xe2\x28\xa1"

    fmt.Println(utf8.ValidString(s)) // false

    fmt.Println(s) // �(�
}
</code></pre>
<h3>7) rune类型</h3>
<p>Go中的Unicode码点以“rune”类型表示，而“rune”又是32位整数。</p>
<h3> <img src='https://tonybai.com/wp-includes/images/smilies/icon_cool.gif' alt='8)' class='wp-smiley' /> 字符串长度</h3>
<p>在字符串上调用len将返回字符串中的字节数，而不是字符数。</p>
<p>获取字符数可能会十分复杂。在您的用例中，对字符串中的rune进行计数可能不够好：</p>
<pre><code>package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    s := "한국어" // 3 Korean characters, encoded in 9 bytes

    byteLen := len(s)
    runeLen := utf8.RuneCountInString(s)
    runeLen2 := len([]rune(s)) // same thing as doing RuneCountInString

    fmt.Println(byteLen, runeLen, runeLen2) // prints 9 3 3
}

</code></pre>
<p>不幸的是，某些Unicode字符跨越多个代码点，因此跨越多个rune。需要做一些可怕的事情来计算出人类能感觉到的Unicode字符串中的字符数。如<a href="http://unicode.org/reports/tr15/#Norm_Forms">Unicode标准中所述</a>。Go库并没有真正提供一种简单的方法。这是方法之一：</p>
<pre><code>package main

import (
    "fmt"
    "unicode/utf8"
    "golang.org/x/text/unicode/norm"
)

func normlen(s string) int {
    var ia norm.Iter
    ia.InitString(norm.NFKD, s)
    nc := 0

    for !ia.Done() {
        nc = nc + 1
        ia.Next()
    }

    return nc
}

func main() {
    str := "é́́" // a particularly strange character

    fmt.Printf(
        "%d bytes, %d runes, %d actual character",
        len(str),
        utf8.RuneCountInString(str),
        normlen(str))
}
</code></pre>
<pre><code>7 bytes, 4 runes, 1 actual character

</code></pre>
<h3>9) 字符串下标运算符 vs. for&#8230;range</h3>
<p>简而言之，字符串下标运算符返回字符串的字节数组下标处的字节。而for range则在字符串中的rune上进行迭代，将字符串解释为UTF-8编码的文本：</p>
<pre><code>package main

import (
    "fmt"
)

func main() {
    s := "touché"

    // prints every byte
    // touchÃ©
    for i := 0; i &lt; len(s); i++ {
        fmt.Print(string(s[i]))
    }
    fmt.Println()

    // prints every rune
    // touché
    for _, r := range s {
        fmt.Print(string(r))
    }
    fmt.Println()

    // convert a string to rune slice to access by index
    // touché
    r := []rune(s)
    for i := 0; i &lt; len(r); i++ {
        fmt.Print(string(r[i]))
    }
}

</code></pre>
<h2>8. map</h2>
<h3>1) map迭代顺序是随机的（不是真的）</h3>
<p>从技术上讲，map的迭代顺序是“未定义的”。Go map在内部使用哈希表，并且通常按照map元素在该表中的排列顺序进行map迭代。当将新元素添加到map时，由于哈希表需要增长，因此不能依赖此顺序并进行更改。在Go的早期，这对于那些不阅读语言文档并以某种方式依赖于按一定顺序进行迭代的程序员来说是一个严重的陷阱。为了帮助及早发现这些问题，而不是在生产中发现这些问题，Go开发人员将map迭代设为随机：</p>
<pre><code>package main

import "fmt"

func main() {
    // add sequential elements
    // to make it seem like maybe maps are iterated in order
    m := map[int]int{0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5}
    for i := 0; i &lt; 5; i++ {
        for i := range m {
            fmt.Print(i, " ")
        }
        fmt.Println()
    }

    // add more elements
    // to make the hash table of the map grow and reorder elements
    m[6] = 6
    m[7] = 7
    m[8] = 8

    for i := 0; i &lt; 5; i++ {
        for i := range m {
            fmt.Print(i, " ")
        }
        fmt.Println()
    }
}

</code></pre>
<pre><code>3 4 5 0 1 2
5 0 1 2 3 4
0 1 2 3 4 5
1 2 3 4 5 0
0 1 2 3 4 5
0 1 3 6 7 2 4 5 8
1 3 6 7 0 4 5 8 2
2 4 5 8 0 1 3 6 7
0 1 3 6 7 2 4 5 8
0 1 3 6 7 2 4 5 8
</code></pre>
<p>在上面的示例中，当使用项目1到5初始化map时，它们将以该顺序添加到哈希表中。前五个打印行都是按顺序写入的数字0到5。Go仅随机化迭代从哪个元素开始。向map添加更多元素会使map的哈希表增加。从而对整个哈希表进行重新排序。最后5条输出不再以任何明显的顺序排列。如果需要，您可以在<a href="https://github.com/golang/go/blob/master/src/runtime/map.go">Go maps</a>的源代码中找到有关它的全部信息。</p>
<h3>2) 检查map键是否存在</h3>
<p>访问不存在的map元素将返回map值类型的默认值(零值)。如果它是整数map，则将返回0，对于引用类型，它将为nil。当您要检查map中是否存在某个元素时，有时默认值就足够了。例如，如果您有一个指向结构的指针的map，然后在访问map时获得nil值，则可以确保这意味着您要查找的元素不在map中。例如，在布尔值map的情况下，默认值不足以判断元素值是否为“false”或map中是否缺少该元素。访问map元素会返回一个可选的第二个参数，该参数可以告诉您该元素是否确实在map中：</p>
<pre><code>package main

import "fmt"

func main() {
    m := map[int]bool{1: false, 2: true, 3: true}

    // prints false, but not clear if the value of
    // the element is false or map item doesn’t exist
    // and the default was returned
    fmt.Println(m[1]) 

    val, exists := m[1]
    fmt.Println(val, exists) // prints false true
}

</code></pre>
<p>切片类型是具有指向数组的指针的结构（值类型），而map本身是指针。切片的零值已经完全可用。您可以使用append添加元素并获取其长度。但map是不同的。Go开发人员希望使map零值完全可用，但他们不知道如何有效地实现它。Go中的Map关键字是&#42;runtime.hmap类型的别名。它的零值为nil。可以读取nilmap，但不能将其写入：</p>
<pre><code>package main

import "fmt"

func main() {
    var m map[int]int // a nil map

     // taking len of a nil map is OK. prints 0
    fmt.Println(len(m))
    // reading nil map is OK. prints 0 (the default of the map value type)
    fmt.Println(m[10])  

    m[10] = 1 // panic: assignment to entry in nil map
}

</code></pre>
<p>可以读取nil map，因为map项是使用类似下面这样的函数访问的（来自runtime/map.go）：</p>
<pre><code>func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer
</code></pre>
<p>此函数将检查map是否为nil，如果为nil，则返回零值。请注意，它无法为您创建map。要拥有完全可用的map，必须调用make：</p>
<pre><code>package main

import "fmt"

func main() {
    m := make(map[int]int)
    m[10] = 11         // now all is well
    fmt.Println(m[10]) // prints 11
}

</code></pre>
<p>由于map是将其传递给函数的指针，因此将指针传递给相同的map数据结构：</p>
<pre><code>package main

import "fmt"

func f1(m map[int]int) {
    m[5] = 123
}

func main() {
    m := make(map[int]int)
    f1(m)
    fmt.Println(m[5]) // prints 123
}

</code></pre>
<p>当将指向map的指针传递给函数时，该指针的值将被复制（Go通过值（包括指针）传递所有内容）。如果要在函数内部创建新的map，它将更改指针副本的值。因此，这将不起作用：</p>
<pre><code>package main

import "fmt"

func f1(m map[int]int) {
    m = make(map[int]int)
    m[5] = 123
}

func main() {
    var m map[int]int
    f1(m)
    fmt.Println(m[5])     // prints 0
    fmt.Println(m == nil) // true
}

</code></pre>
<h3>3) struct{}类型</h3>
<p>Go没有集合数据结构（类似于带有键的map，但没有C++中实现的std::set或在C＃中实现的HashSet）。使用map替代非常简单。一个小技巧是在这种情况下使用struct{}类型作为map值：</p>
<pre><code>package main

import (
    "fmt"
)

func main() {
    m := make(map[int]struct{})
    m[123] = struct{}{}
    _, keyexists := m[123]
    fmt.Println(keyexists)
}

</code></pre>
<p>通常在此处会使用bool值，但是具有struct{}值类型的map将使用更少的内存。struct{}类型实际上为零字节：</p>
<pre><code>package main

import (
    "fmt"
    "unsafe"
)

func main() {
    fmt.Println(unsafe.Sizeof(false)) // 1
    fmt.Println(unsafe.Sizeof(struct{}{})) // 0
}
</code></pre>
<h3>4) map容量</h3>
<p>map是一个相当复杂的数据结构。虽然可以在创建map时指定map的初始容量，但以后无法获取其容量（至少不能使用cap函数）：</p>
<pre><code>package main

import (
    "fmt"
)

func main() {
    m := make(map[int]bool, 5) // initial capacity of 5
    fmt.Println(len(m)) // len is fine
    fmt.Println(cap(m)) // invalid argument m (type map[int]bool) for cap
}

</code></pre>
<h3>5) map值不可寻址</h3>
<p>Go Map是用哈希表实现的，当map需要增长或缩小时，哈希表需要移动其元素。因此，Go不允许使用map元素的地址：</p>
<pre><code>package main

import "fmt"

type item struct {
    value string
}

func main() {
    m := map[int]item{1: {"one"}}

    fmt.Println(m[1].value) // reading a struct value is fine
    addr := &amp;m[1]           // error: cannot take the address of m[1]
    // error: cannot assign to struct field m[1].value in map
    m[1].value = "two"
}

</code></pre>
<p>有一种<a href="https://github.com/golang/go/issues/3117">建议允许给一个结构体字段赋值</a>（m[1].value =”two”），因为在这种情况下，不保留指向值字段的指针，只能通过它进行分配。尽管由于“微不足道的情况”，目前尚无具体计划何时或是否实施。</p>
<p>解决方法是将整个结构重新分配回map：</p>
<pre><code>package main

type item struct {
    value string
}

func main() {
    m := map[int]item{1: {"one"}}
    tmp := m[1]
    tmp.value = "two"
    m[1] = tmp
}

</code></pre>
<p>或者，指向结构体的指针map也将起作用。在这种情况下，m[1]的“值”的类型为&#42;item。Go不需要带一个指向map值的指针，该值本身已经是一个指针。哈希表将在内存中移动指针，但是如果您复制值m[1]的副本，它将继续指向同一项目，因此一切都很好：</p>
<pre><code>package main

import "fmt"

type item struct {
    value string
}

func main() {
    m := map[int]*item{1: {"one"}}
    // Go does not need to take address of m[1] here
    // as it is a pointer already
    m[1].value = "two"
    fmt.Println(m[1].value) // two

    addr := &amp;m[1] // still same error: cannot take the address of m[1]
}

</code></pre>
<p>值得注意的是，切片和数组没有此问题：</p>
<pre><code>package main

import "fmt"

func main() {
    slice := []string{"one"}

    saddr := &amp;slice[0]
    *saddr = "two"

    fmt.Println(slice) // [two]
}
</code></pre>
<h3>6) 数据竞态</h3>
<p>常规的Go map对于并发访问并不安全。map通常用于在goroutine之间共享数据，但是对map的访问必须通过sync.Mutex，sync.RWMutex，其他一些内存屏障或与Go channel进行协调来阻止并发访问。除了以下例外：</p>
<blockquote>
<p>仅当发生更新时，map访问才是不安全的。只要所有goroutine仅读取（在map中查找元素，包括使用for range 循环对其进行遍历）， 并且不通过分配元素或进行删除来更改map，则对于它们来说，在不同步的情况下并发访问map是安全的。 &#8211; https://golang.org/doc/faq</p>
</blockquote>
<pre><code>package main

import (
    "math/rand"
    "time"
)

func readWrite(m map[int]int) {
    // do some random reads and writes to the map
    for i := 0; i &lt; 100; i++ {
        k := rand.Int()
        m[k] = m[k] + 1
    }
}

func main() {
    m := make(map[int]int)

    // start goroutines to read and write map concurrently
    for i := 0; i &lt; 10; i++ {
        go readWrite(m)
    }

    time.Sleep(time.Second)
}

</code></pre>
<pre><code>fatal error: concurrent map read and map write
fatal error: concurrent map writes
…
</code></pre>
<p>在这种情况下，map访问可以用互斥对象同步。下面的代码将按预期工作：</p>
<pre><code>package main

import (
    "math/rand"
    "sync"
    "time"
)

var mu sync.Mutex

func readWrite(m map[int]int) {
    mu.Lock()
    // defer unlock mutex will unlock mutex
    // even if this goroutine would panic
    defer mu.Unlock()

    for i := 0; i &lt; 100; i++ {
        k := rand.Int()
        m[k] = m[k] + 1
    }
}

func main() {
    m := make(map[int]int)
    for i := 0; i &lt; 10; i++ {
        go readWrite(m)
    }

    time.Sleep(time.Second)
}

</code></pre>
<h3>7) sync.Map</h3>
<p>sync包中有一个特殊版本的map，可以安全地被多个goroutine并发使用。但是，Go文档建议在大多数情况下使用Mutex的常规map。sync.Map是类型不安全的，它类似于map[interface{}]interface{}。sync.Map文档有这段描述：</p>
<blockquote>
<p>Map类型针对两种常见使用场景进行了优化：（1）给定键的条目仅写入一次但读取多次，例如在仅增长的高速缓存中；（2）当多个goroutine进行读取，写入和覆盖不相交的键集的条目。在这两种情况下，与单独的Mutex或RWMutex配对的Go map相比，使用Map可以显着减少锁争用。 &#8211; https://github.com/golang/go/blob/master/src/sync/map.go</p>
</blockquote>
<h2>9. 循环(loop)</h2>
<h3>1) range迭代器返回两个值</h3>
<p>初学者的陷阱。Go中的For-range与其他语言中的for-range略有不同。它返回一个或两个变量，第一个是迭代索引（如果迭代的对象是map，第一个值则是map键），第二个是值。如果仅使用一个变量，那么它是索引：</p>
<pre><code>package main

import "fmt"

func main() {
    slice := []string{"one", "two", "three"}

    for v := range slice {
        fmt.Println(v) // 0, 1, 2
    }

    for _, v := range slice {
        fmt.Println(v) // one two three
    }
}

</code></pre>
<h3>2) For循环迭代器变量被重用</h3>
<p>在循环中，每次迭代都重复使用相同的迭代器变量。如果使用其地址，则每次都将是相同的地址，这意味着迭代器变量的值将在每次迭代时复制到相同的内存位置。它使循环更有效，但<strong>它也是Go中最常见的陷阱之一</strong>。这是Go Wiki的示例：</p>
<pre><code>package main

import "fmt"

func main() {
    var out []*int
    for i := 0; i &lt; 3; i++ {
        out = append(out, &amp;i)
    }
    fmt.Println("Values:", *out[0], *out[1], *out[2])
    fmt.Println("Addresses:", out[0], out[1], out[2])
}
</code></pre>
<pre><code>Values: 3 3 3
Addresses: 0xc0000120e0 0xc0000120e0 0xc0000120e0
</code></pre>
<p>一种解决方案是在循环内部声明一个新变量。在代码块内部声明的变量即使在循环中也不会被重用：</p>
<pre><code>package main

import "fmt"

func main() {
    var out []*int
    for i := 0; i &lt; 3; i++ {
        i := i // copy i into a new variable
        out = append(out, &amp;i)
    }
    fmt.Println("Values:", *out[0], *out[1], *out[2])
    fmt.Println("Addresses:", out[0], out[1], out[2])
}
</code></pre>
<p>现在，它可以按预期工作：</p>
<pre><code>Values: 0 1 2
Addresses: 0xc0000120e0 0xc0000120e8 0xc0000120f0
</code></pre>
<p>在使用for-range子句的情况下，将同时使用索引和值变量。</p>
<p>在循环中启动goroutine是类似的事情：</p>
<pre><code>package main

import (
    "fmt"
    "time"
)

func main() {
    for i := 0; i &lt; 3; i++ {
        go func() {
            fmt.Print(i)
        }()
    }
    time.Sleep(time.Second)
}
</code></pre>
<pre><code>333
</code></pre>
<p>这些goroutine是在此循环中创建的，但是它们开始运行需要花费一些时间。由于它们捕获单个i变量，因此Println会在执行goroutine时打印其具有的任何值。</p>
<p>在这种情况下，您可以像前面的示例一样在代码块内创建一个新变量，或者将iterator变量作为参数传递给goroutine：</p>
<pre><code>package main

import (
    "fmt"
    "time"
)

func main() {
    for i := 0; i &lt; 3; i++ {
        go func(i int) {
            fmt.Print(i)
        }(i)
    }
    time.Sleep(time.Second)
}
</code></pre>
<pre><code>012

</code></pre>
<p>在这里，goroutine i的参数是一个新变量，它是从迭代器变量中复制的，这是创建goroutine的一部分。</p>
<p>如果循环不是启动goroutine，而是调用一个简单的函数，则代码将按预期工作：</p>
<pre><code>for i := 0; i &lt; 3; i++ {
    func() {
        fmt.Print(i)
    }()
}

</code></pre>
<pre><code>012
</code></pre>
<p>变量i像以前一样被重用。但是，这些函数调用中的每一个都不会让循环继续进行，直到函数完成执行为止，在这段时间内，我将获得期望的值。</p>
<p>它变得有些棘手。通过在struct上调用一个方法来看看这个例子：</p>
<pre><code>package main

import (
    "fmt"
    "time"
)

type myStruct struct {
    v int
}

func (s *myStruct) myMethod() {
    // print the value of myStruct and its address
    fmt.Printf("%v, %p\n", s.v, s)
}

func main() {
    byValue := []myStruct{{1}, {2}, {3}}
    byReference := []*myStruct{{1}, {2}, {3}}

    fmt.Println("By value")

    for _, i := range byValue {
        go i.myMethod()
    }
    time.Sleep(time.Millisecond * 100)

    fmt.Println("By reference")

    for _, i := range byReference {
        go i.myMethod()
    }
    time.Sleep(time.Millisecond * 100)
}

</code></pre>
<pre><code>By value
3, 0xc000012120
3, 0xc000012120
3, 0xc000012120
By reference
1, 0xc0000120e0
3, 0xc0000120f0
2, 0xc0000120e8

</code></pre>
<p>我们看到：通过引用使用myStruct时，它的工作就像没有陷阱一样！这与创建goroutines有关。在创建goroutine时会对goroutine参数进行求值。方法接收者（myMethod的myStruct）实际上是一个参数。</p>
<p>当按值调用时：由于myMethod的参数s是一个指针，因此i的地址被视为作为参数传递给goroutine，我们知道迭代器变量被重用，因此每次它都是相同的地址。当迭代器运行时，它将复制新的myStruct值到i变量的相同地址。打印的值是执行goroutine时i变量具有的值。</p>
<p>当通过引用调用时：参数已经是一个指针，因此在创建goroutine时将指针值压入新的goroutine的堆栈中。这恰好是我们想要的地址，并打印了期望值。</p>
<h3>3) 带label的break和continue</h3>
<p>Go可能鲜为人知的功能是能够为for, switch和select语句加上label，并在这些label上使用break和continue，这是我们常用的跳出外循环的方法：</p>
<pre><code>loopi:
    for x := 0; x &lt; 3; x++ {
        for y := 0; y &lt; 3; y++ {
            fmt.Printf(x, y)
            break loopi
        }
    }

</code></pre>
<pre><code>0 0
</code></pre>
<p>continue也可以类似的方式使用：</p>
<pre><code>loopi:
    for x := 0; x &lt; 3; x++ {
        for y := 0; y &lt; 3; y++ {
            fmt.Printf(x, y)
            continue loopi
        }
    }
</code></pre>
<pre><code>0 0
1 0
2 0

</code></pre>
<p>label也可以与switch和select语句一起使用。在这里，没有label的break只会脱离select语句并进入for循环：</p>
<pre><code>package main

import (
    "fmt"
    "time"
)

func main() {
loop:
    for {
        select {
        case &lt;-time.After(time.Second):
            fmt.Println("timeout reached")
            break loop
        }
    }
    fmt.Println("the end")
}

</code></pre>
<pre><code>timeout reached
the end

</code></pre>
<p>正如前面提到的，switch和select语句也可以加上label，因此我们可以将上面的示例写成：</p>
<pre><code>package main

import (
    "fmt"
    "time"
)

func main() {
myswitch:
    switch {
    case true:
        for {
            fmt.Println("switch")
            break myswitch // would not be able to “continue” in this case
        }
    }
    fmt.Println("the end")
}

</code></pre>
<pre><code>switch
the end
</code></pre>
<p>容易将前面示例中的“label语句”与将使用goto的label混淆。实际上，您可以对break/continue和goto使用相同的标签，但是行为会有所不同。在下面的代码中，虽然break会脱离标记循环，但是goto会将代码执行转移到标签的位置（并在下面的代码中导致无限循环）：</p>
<pre><code>package main

import (
    "fmt"
    "time"
)

func main() {
loop:
    switch {
    case true:
        for {
            fmt.Println("switch")
            break loop // breaks the “labeled statement”
        }
    }
    fmt.Println("not the end")
    goto loop // jumps to “loop” label
}

</code></pre>
<pre><code>switch
not the end
switch
not the end
…

</code></pre>
<h2>10. Switch和Select</h2>
<h3>1) case语句会默认break</h3>
<p>与基于C的语言不同，Go中的case语句默认情况下会break。要使case语句向下继续执行，请使用fallthrough关键字：</p>
<pre><code>package main

import (
    "fmt"
    "time"
)

func main() {
    // this will not work, in case of Saturday nothing will be printed
    switch time.Now().Weekday() {
    case 6: // this case will break out of switch without doing anything
    case 7:
        fmt.Println("weekend")
    }

    switch time.Now().Weekday() {
    case 1:
        break // this break does nothing because case would break anyway
    case 2:
        fmt.Println("weekend")
    }

    // fallthrough keyword will make Saturday print weekend as well
    switch time.Now().Weekday() {
    case 6:
        fallthrough
    case 7:
        fmt.Println("weekend")
    }

    // case can also have multiple values
    switch time.Now().Weekday() {
    case 6, 7:
        fmt.Println("weekend")
    }

    // conditional breaks are still useful
    switch time.Now().Weekday() {
    case 6, 7:
        day := time.Now().Format("01-02")
        if day == "12-25" || day == "12-26" {
            fmt.Println("Christmas weekend")
            break // do not also print "weekend"
        }

        // a regular weekend
        fmt.Println("weekend")
    }
}

</code></pre>
<h3>2) 带label的break</h3>
<p>如之前在循环一章中提到的那样，switch和select也可以执行带label的break来中断外部循环，而不是switch或select语句本身：</p>
<pre><code>package main

import (
    "fmt"
    "strings"
)

func main() {
    s := "The quick brown Waldo fox jumps over the lazy dog"

findWaldoLoop:
    for _, w := range strings.Split(s, " ") {
        switch w {
        case "Waldo":
            fmt.Println("found Waldo!")
            break findWaldoLoop
        default:
            fmt.Println(w, "is not Waldo")
        }
    }
}

</code></pre>
<pre><code>The is not Waldo
quick is not Waldo
brown is not Waldo
found Waldo!

</code></pre>
<h2>11. 函数</h2>
<h3>1) defer语句</h3>
<p>Defer似乎没有很大的陷阱，但还是有值得一提的是细微之处。</p>
<p>摘自<a href="https://blog.golang.org/defer-panic-and-recover">安德鲁·格朗（Andrew Gerrand）</a>关于该主题的出色文章：</p>
<blockquote>
<p>defer语句将函数调用推送到列表上。包裹函数返回后，将执行保存的呼叫列表。Defer通常用于简化执行各种清理操作的功能。</p>
</blockquote>
<p>要注意的最重要的几点：</p>
<ul>
<li>虽然在原始函数返回时调用了deferred函数，但在调用defer时会对其参数求值</li>
</ul>
<pre><code>package main

import (
    "fmt"
)

func main() {
    s := "defer"
    defer fmt.Println(s)
    s = "original"
    fmt.Println(s)
}
</code></pre>
<pre><code>original
defer
</code></pre>
<ul>
<li>原始函数返回后，延迟函数将按照后进先出的顺序执行</li>
</ul>
<pre><code>package main

import (
    "fmt"
)

func main() {
    defer fmt.Println("one")
    defer fmt.Println("two")
    defer fmt.Println("three")
}

</code></pre>
<pre><code>three
two
one
</code></pre>
<ul>
<li>延迟函数可以访问和修改命名函数参数</li>
</ul>
<pre><code>package main

import (
    "fmt"
    "time"
)

func timeNow() (t string) {
    defer func() {
      t = "Current time is: " + t
    }()
  return time.Now().Format(time.Stamp)
}

func main() {
    fmt.Println(timeNow())
}

</code></pre>
<pre><code>Current time is: Feb 13 13:36:44

</code></pre>
<ul>
<li>Defer不适用于代码块，仅适用于整个函数</li>
</ul>
<p>与变量声明不同，defer语句的作用域不限于代码块：</p>
<pre><code>package main

import (
    "fmt"
)

func main() {
    for i := 0; i &lt; 9; i++ {
        if i%3 == 0 {
            defer func(i int) {
                fmt.Println("defer", i)
            }(i)
          }
    }
    fmt.Println("exiting main")
}

</code></pre>
<pre><code>exiting main
defer 6
defer 3
defer 0
</code></pre>
<p>在此示例中，当i为0、3和6时，延迟函数调用将添加到列表中。但是，仅当主函数退出时（而不是在if语句的末尾，即离开代码块时）才调用该函数。</p>
<ul>
<li>recover函数仅在延迟函数内部起作用，而在原始函数中则无济于事</li>
</ul>
<p>它实际上没有其他任何意义，但是如果您正在寻找等效的try &#8230; catch语句，那么Go中没有这种语句。Go使用延迟函数内部的recover捕获panic。</p>
<pre><code>package main

import (
    "fmt"
)

func panickyFunc() {
    panic("panic!")
}

func main() {
    defer func() {
      r := recover()
      if r != nil {
        fmt.Println("recovered", r)
      }
    }()

    panickyFunc()

    fmt.Println("this will never be printed")
}

</code></pre>
<pre><code>recovered panic!

</code></pre>
<h2>12. Goroutines</h2>
<h3>1) 什么是goroutines</h3>
<p>在大多数情况下，goroutines可以视为轻量级线程。它们可以快速启动，最初只使用2kb的堆栈内存（可以增加或缩小）。它们由Go运行时（而不是操作系统）管理，它们之间的上下文切换损耗很低。Goroutine是为并发而构建的，当在多个硬件线程上运行时，它们还将并行运行。</p>
<blockquote>
<p>并发就是一次处理很多事情。并行是关于一次做很多事情 &#8211; 罗伯·派克</p>
</blockquote>
<p>它们的效率令人吃惊，当与channel结合使用时，它们很可能是Go的最佳特性。它们在Go中无处不在，但是goroutine的一个好问题的一个极端示例可能是管理大量并发Websocket连接的服务器。它们需要分别进行单独管理，但是它们也很可能大部分闲置（不占用大量CPU或内存）。为每个线程创建一个线程，一旦连接到数千个连接都会引起问题，而使用goroutine可能会产生数十万个连接。</p>
<p>关于goroutines如何工作的<a href="https://medium.com/technofunnel/understanding-golang-and-goroutines-72ac3c9a014d">更详细的帖子可以在这里找到</a>。</p>
<h3>2) 运行goroutines不会阻止程序退出</h3>
<p>当主函数退出时，Go程序退出。在后台运行的所有goroutine都会安静地停止。以下程序将退出而不打印任何内容</p>
<pre><code>package main

import (
    "fmt"
    "time"
)

func goroutine1() {
    time.Sleep(time.Second)
    fmt.Println("goroutine1")
}

func goroutine2() {
    time.Sleep(time.Second)
    fmt.Println("goroutine2")
}

func main() {
    go goroutine1()
    go goroutine2()
}

</code></pre>
<p>为了确保这些goroutine完成，需要添加一些同步措施，例如使用channel或sync.WaitGroup：</p>
<pre><code>package main

import (
    "fmt"
    "sync"
    "time"
)

func goroutine1(wg *sync.WaitGroup) {
    time.Sleep(time.Second)
    fmt.Println("goroutine1")
    wg.Done()
}

func goroutine2(wg *sync.WaitGroup) {
    time.Sleep(time.Second)
    fmt.Println("goroutine2")
    wg.Done()
}

func main() {
    wg := &amp;sync.WaitGroup{}
    wg.Add(2)

    go goroutine1(wg)
    go goroutine2(wg)

    wg.Wait()
}

</code></pre>
<pre><code>goroutine2
goroutine1
</code></pre>
<h3>3) panic的goroutine会使整个应用程序崩溃</h3>
<p>goroutine中的panic情况必须使用defer和recover处理。否则，整个应用程序将崩溃：</p>
<pre><code>package main

import (
    "fmt"
    "time"
)

func goroutine1() {
    panic("something went wrong")
}

func main() {
    go goroutine1()
    time.Sleep(time.Second)
    fmt.Println("will never get here")
}

</code></pre>
<pre><code>panic: something went wrong

goroutine 6 [running]:
main.goroutine1()
        c:/projects/test/main.go:9 +0x45
created by main.main
        c:/projects/test/main.go:13 +0x45

</code></pre>
<h2>13. 接口</h2>
<h3>1) 检查接口变量是否为nil</h3>
<p>这无疑是Go中最常见的陷阱之一。Go中的接口不像某些其他语言，它不仅仅是指向内存位置的指针。</p>
<p>Go接口具有：</p>
<ul>
<li>静态类型（接口本身的类型）</li>
<li>动态类型</li>
<li>值(value)</li>
</ul>
<blockquote>
<p>接口类型的变量的动态类型和值均为nil时，其值才等于nil</p>
</blockquote>
<pre><code>package main

import (
    "fmt"
)

type ISayHi interface {
    Say()
}

type SayHi struct{}

func (s *SayHi) Say() {
    fmt.Println("Hi!")
}

func main() {
    // at this point variable “sayer” only has the static type of ISayHi
    // dynamic type and value are nil
    var sayer ISayHi

    // as expected sayer equals to nil
    fmt.Println(sayer == nil) // true

    // a nil variable of a concrete type
    var sayerImplementation *SayHi

    // dynamic type of the interface variable is now SayHi
    // the actual value interface points to is still nil
    sayer = sayerImplementation

    // sayer no longer equals to nil, because its dynamic type is set
    // even though the value it points to is nil
    // which is not what most people would expect here
    fmt.Println(sayer == nil) // false
}
</code></pre>
<p>接口值设置为nil结构体。接口不能用于任何东西，那么为什么它不等于nil？与其他语言相比，这是Go的另一个区别。在C＃中对nil类调用方法时，无论情况好坏，都会引发异常，在Go中它是允许的。因此，当接口设置了动态类型时，即使该值为nil，有时也可以使用。因此，您可以争辩说接口不是真的“nil”：</p>
<pre><code>package main

import (
    "fmt"
)

type ISayHi interface {
    Say()
}

type SayHi struct{}

func (s *SayHi) Say() {
    // this function is not accessing s
    // even if s is nil this will work
    fmt.Println("Hi!")
}

func main() {
    var sayer ISayHi
    var sayerImplementation *SayHi
    sayer = sayerImplementation

    // the value of SayHi on sayer interface is nil
    // in Go it's OK to call methods on a nil struct
    // this line will work fine, because Say function is not accessing s
    sayer.Say()
}

</code></pre>
<p>奇怪的是，没有简单的方法可以检查接口指向的值是否为nil。关于该主题的讨论正在进行很长时间，而且似乎没有任何进展。因此，在可预见的将来，您可以执行以下操作：</p>
<ul>
<li>最少的选择1：永远不要将具体类型的零值赋值给接口</li>
</ul>
<p>如果您从不将具体类型的零值赋值给接口变量（设计用于nil接收器的类型除外），则简单的“== nil”检查将始终有效。例如，永远不要这样做：</p>
<pre><code>func MyFunc() ISayHi {
    var result *SayHi
    if time.Now().Weekday() == time.Sunday {
      result = &amp;SayHi{}
    }
    // if it’s not Sunday, this returns an interface that is not
    // equal to nil, but has a nil value for its concrete type
    // (MyFunc() == nil would be false)
    return result
}

</code></pre>
<p>而是返回实际的nil：</p>
<pre><code>func MyBetterFunc() ISayHi {
    if time.Now().Weekday() != time.Sunday {
      // if it’s not Sunday
      // MyBetterFunc() == nil would be true
      return nil
  }
    return &amp;SayHi{}
}

</code></pre>
<p>即使它不是理想的，它也可能是最好的可用解决方案，因为那时每个人都必须意识到它，并在代码审查等中对其进行监视，并以某种方式完成计算机可以完成的工作。</p>
<ul>
<li>在特殊情况下可以选择2：反射</li>
</ul>
<p>如果需要，可以通过反射检查接口的基础值是否为零。这会很慢，并且用以下函数调用来填充代码可能不是一个好主意：</p>
<pre><code>func IsInterfaceNil(i interface{}) bool {
    if i == nil {
      return false
    }
    rvalue := reflect.ValueOf(i)
    return rvalue.Kind() == reflect.Ptr &amp;&amp; rvalue.IsNil()
}
</code></pre>
<p>检查value的Kind()是否是指针是必要的，因为IsNil会对无法为nil的类型（例如简单的int）抛出panic。</p>
<ul>
<li>请不要执行此选项3：将IsNil添加到您的struct接口中</li>
</ul>
<p>这样，您可以在不使用反射的情况下检查接口是否为零：</p>
<pre><code>type ISayHi interface {
    Say()
    IsNil() bool
}

type SayHi struct{}

func (s *SayHi) Say() {
    fmt.Println("Hi!")
}

func (s *SayHi) IsNil() bool {
    return s == nil
}

</code></pre>
<ul>
<li>也许考虑选项1和选项4：声明具体类型</li>
</ul>
<p>如果知道接口值应该是哪种类型，则可以通过首先使用类型开关(type switch)或类型断言获取具体类型的值来检查接口值是否为零：</p>
<pre><code>func main() {
    v := MyFunc()
    fmt.Println(v.(*SayHi) == nil)
}

</code></pre>
<p>如果您真的知道自己在做什么，可能会很好，但是在许多情况下，这种方法超出了使用接口开始的目的。考虑添加ISayHi的新实现时会发生什么。您是否需要记住查找此代码并为新结构添加另一个检查？您会为每个新实现执行此操作吗？如果此代码正在处理很少发生的事件并且仅在代码投入生产后很长时间才发现未检查新添加的实现，该怎么办？</p>
<h3>2) 接口隐式满足</h3>
<p>与许多其他语言不同，您不需要显式指定结构体实现接口。编译器可以自己做出来。这非常有意义，也是非常方便的做法：</p>
<pre><code>package main

import (
    "fmt"
)

// an interface
type ISayHi interface {
    Say()
}

// this struct implements ISayHi even if it doesn't know it
type SayHi struct{}

func (s *SayHi) Say() {
    fmt.Println("Hi!")
}

func main() {
    var sayer ISayHi // sayer is an interface
    sayer = &amp;SayHi{} // SayHi implicitly implements ISayHi
    sayer.Say()
}

</code></pre>
<p>有时让编译器检查结构体是否实现了接口可能会很有用：</p>
<pre><code>// verify at compile time that *SayHi implements ISayHi
var _ ISayHi = (*SayHi)(nil) 

</code></pre>
<h3>3) 在错误类型上的类型断言</h3>
<p>有一个单变量和两变量版本的类型断言。当动态类型与要断言的类型不匹配时，单变量版本会抛出panic：</p>
<pre><code>func main() {
      var sayer ISayHi
      sayer = &amp;SayHi{}

      // t will be a zero value (nil in this case) of type *SayHi2
      // ok will be false
      t, ok := sayer.(*SayHi2)
      if ok {
          t.Say()
      }

      // panic: interface conversion:
      // main.ISayHi is *main.SayHi, not *main.SayHi2
      t2 := sayer.(*SayHi2)
      t2.Say()
}

</code></pre>
<h2>14 继承</h2>
<h3>1) 重新定义与嵌入类型</h3>
<p>Go类型系统是&#8230;务实。它不是面向对象的，而C++或Java是面向对象的。你真的不能继承结构体或接口（没有子类），但你可以把它们放在一起（嵌入），以生成更多的复杂结构体或接口。</p>
<blockquote>
<p>嵌入与子类是两种不同的方式。当我们嵌入一个类型时，该类型的方法成为外部类型的方法，但是当调用它们时，该方法的接收者是内部类型，而不是外部类型。- https://golang.org/doc/effective_go</p>
</blockquote>
<p>在嵌入类型旁边，Go允许重新定义类型。重新定义继承类型的字段，但不继承其方法</p>
<pre><code>package main

type t1 struct {
    f1 string
}

func (t *t1) t1method() {
}

// embedding type
type t2 struct {
    t1
}

// redefining type
type t3 t1 

func main() {
    var mt1 t1
    var mt2 t2
    var mt3 t3

    // fields are inherited in all the cases
    _ = mt1.f1
    _ = mt2.f1
    _ = mt3.f1

    // these work ok
    mt1.t1method()
    mt2.t1method()

    // mt3.t1method undefined (type t3 has no field or method t1method)
    mt3.t1method()
}

</code></pre>
<h2>15. 相等性</h2>
<h3>1) Go的相等性</h3>
<p>在Go中比较事物的方式有多种，但没有一种是完美的。</p>
<h3>2) 运算符==和!=</h3>
<p>相等运算符是在Go中比较事物的最简单且通常是最有效的方法，但它仅适用于某些事物。最值得注意的是，它不适用于切片或map。切片和map只能以这种方式与nil进行比较。</p>
<p>使用==可以比较基本类型，例如int和string，还可以比较其中包含可以使用==进行比较的元素的数组和结构：</p>
<pre><code>package main

import "fmt"

type compareStruct1 struct {
    A int
    B string
    C [3]int
}

func main() {
    s1 := compareStruct1{}
    s2 := compareStruct1{}
    fmt.Println(s1 == s2) // works fine, prints true
}

</code></pre>
<p>一旦将无法使用==比较的字段添加到结构体中，就需要使用其他方法进行比较：</p>
<pre><code>package main

import "fmt"

type compareStruct2 struct {
    A int
    B string
    C []int // changed type of C from array to slice
}

func main() {
    s1 := compareStruct2{}
    s2 := compareStruct2{}

    // invalid operation: s1 == s2
    // (struct containing []int cannot be compared)
    fmt.Println(s1 == s2)
}

</code></pre>
<h3>2. 编写特定代码</h3>
<p>如果性能很重要，并且您需要比较稍微复杂一些的类型，那么最好的选择就是手动比较：</p>
<pre><code>type compareStruct struct {
    A int
    B string
    C []int
}

func (s *compareStruct) Equals(s2 *compareStruct) bool {
    if s.A != s2.A || s.B != s2.B || len(s.C) != len(s2.C) {
        return false
    }

    for i := 0; i &lt; len(s.C); i++ {
        if s.C[i] != s2.C[i] {
            return false
        }
    }

    return true
}
</code></pre>
<p>上面代码中的比较功能可以自动生成，但是在撰写本文时，我还不知道有哪个工具可以做到这一点。</p>
<h3>3) Reflection.DeepEqual</h3>
<p>DeepEqual是在Go中比较事物的最通用方法，它可以处理大多数事物。但重点是：</p>
<pre><code>var (
    c1 = compareStruct{
        A: 1,
        B: "hello",
        C: []int{1, 2, 3},
    }
    c2 = compareStruct{
        A: 1,
        B: "hello",
        C: []int{1, 2, 3},
    }
)

func BenchmarkManual(b *testing.B) {
    for i := 0; i &lt; b.N; i++ {
        c1.Equals(&amp;c2)
    }
}

func BenchmarkDeepEqual(b *testing.B) {
    for i := 0; i &lt; b.N; i++ {
        reflect.DeepEqual(c1, c2)
    }
}
</code></pre>
<pre><code>BenchmarkManual-8 217182776 5.51 ns/op 0 B/op 0 allocs/op
BenchmarkDeepEqual-8 2175002 559 ns/op 144 B/op 8 allocs/op
</code></pre>
<p>在此示例中，DeepEqual的速度比手工比较慢了100倍。</p>
<p>请注意，DeepEqual还将比较结构体中的未导出（头母小写）的字段。而且，即使两个不同的类型具有相同字段及值，也永远不会被视为相等。</p>
<h3>4) 无法比较的事情</h3>
<p>有些事情无法比较，甚至与自己都不相等。例如，具有NaN值的浮点变量或func类型。例如，如果在结构体中具有此类字段，则使用DeepEqual比较该结构将不等于其自身：</p>
<pre><code>func TestF(t *testing.T) {
    x := math.NaN
    fmt.Println(reflect.DeepEqual(x, x)) // false
    fmt.Println(reflect.DeepEqual(TestF, TestF)) // false
}

</code></pre>
<h3>5) bytes.Equal</h3>
<p>bytes.Equal是比较字节切片的一种特殊方法。这比简单地将两个切片与使用for循环进行比较要快得多。</p>
<p>值得一提的是bytes.Equal函数认为empty slice和nil slice相等，而reflect.DeepEqual则认为不相等。</p>
<h2>16.内存管理</h2>
<h3>1) 结构体应该通过值还是通过引用传递</h3>
<p>Go函数的参数始终按值传递。当将结构体（或数组）类型变量传递给函数时，整个结构都将被复制。如果传递了指向结构的指针，则会复制该指针，但指向它的结构体不会被复制。复制8个字节的内存（对于64位体系结构），而不用考虑该结构的大小。那么这是否意味着最好将结构作为指针传递？看下面考量。</p>
<p>获取指向结构（或数组）的指针意味：</p>
<ul>
<li>将其放置在堆内存中，而不是通常放在栈中</li>
<li>垃圾收集器来管理该堆分配</li>
</ul>
<p>如果您想复习一下堆与栈的差别，请看看<a href="https://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap">stackoverflow上的这个帖子</a>。就本章而言，了解这些就足够了：栈-快，堆-慢。</p>
<p>这意味着，如果您只是分配结构体，而不是将其作为参数传递，则可以更快地将它们复制到栈中：</p>
<pre><code>package test

import (
    "testing"
)

type myStruct struct {
    a, b, c int64
    d, e, f string
    g, h, i float64
}

func byValue() myStruct {
    return myStruct{
        a: 1, b: 1, c: 1,
        d: "foo", e: "bar", f: "baz",
        g: 1.0, h: 1.0, i: 1.0,
    }
}

func byReference() *myStruct {
    return &amp;myStruct{
        a: 1, b: 1, c: 1,
        d: "foo", e: "bar", f: "baz",
        g: 1.0, h: 1.0, i: 1.0,
    }
}

func BenchmarkByValue(b *testing.B) {
    var s myStruct

    for i := 0; i &lt; b.N; i++ {
        // make a copy of the whole struct
        // but do it through stack memory
        s = byValue()
    }

    _ = s
}

func BenchmarkByReference(b *testing.B) {
    var s *myStruct

    for i := 0; i &lt; b.N; i++ {
        // allocate struct on the heap
        // and only return a pointer to it
        s = byReference()
    }

    _ = s
}

</code></pre>
<pre><code>BenchmarkByValue-8 476965734 2.499 ns/op 0 B/op 0 allocs/op
BenchmarkByReference-8 24860521 45.86 ns/op 96 B/op 1 allocs/op

</code></pre>
<p>在这个demo示例中，按值传递（不涉及堆或垃圾收集器）的速度快18倍。</p>
<p>为了说明这一点，让我们做一个相反的示例，一次分配该结构，然后仅将其传递给函数：</p>
<pre><code>var s = myStruct{
    a: 1, b: 1, c: 1,
    d: "foo", e: "bar", f: "baz",
    g: 1.0, h: 1.0, i: 1.0,
}

func byValue() myStruct {
    return s
}

func byReference() *myStruct {
    return &amp;s
}

</code></pre>
<pre><code>BenchmarkByValue-8 471494428 2.509 ns/op 0 B/op 0 allocs/op
BenchmarkByReference-8 1000000000 0.2484 ns/op 0 B/op 0 allocs/op

</code></pre>
<p>当只传递而不是分配时，通过引用它会更快。</p>
<p>有关更多详细信息，请查看<a href="https://medium.com/a-journey-with-go/go-should-i-use-a-pointer-instead-of-a-copy-of-my-struct-44b43b104963">Vincent Blanchon撰写的精彩文章</a>。</p>
<p>尽管本章讨论的是哪一个更快，但是在许多应用程序中，代码的清晰度和一致性比性能更重要，但这是一个单独的讨论。总之，不要以为复制会很慢，如果性能很重要，请使用<a href="https://www.imooc.com/read/87/article/2440">Go profiler</a>。</p>
<h3>2) 给C开发人员的提示</h3>
<p>Go在内存管理上要严格得多。不允许使用指针算术，并且不可能有悬空的指针。这样的事情非常好：</p>
<pre><code>func byReference() *myStruct {
    return &amp;myStruct{
        a: 1, b: 1, c: 1,
        d: "foo", e: "bar", f: "baz",
        g: 1.0, h: 1.0, i: 1.0,
    }
}
</code></pre>
<p>Go编译器足够聪明，可以将结构移动到堆中。</p>
<h2>17. 日志</h2>
<h3>1) log.Fatal和log.Panic</h3>
<p>使用Go日志包记录日志时，log.Fatal和log.Panic函数中有一个陷阱在等你。与您可能期望的日志记录功能不同，它们不只是简单地记录具有不同日志级别的消息，它们还会终止整个应用程序。以下是Go日志包中的这两个函数的定义：</p>
<pre><code>// Fatal is equivalent to Print() followed by a call to os.Exit(1).
func Fatal(v ...interface{}) {
    std.Output(2, fmt.Sprint(v...))
    os.Exit(1)
}

// Panic is equivalent to Print() followed by a call to panic().
func Panic(v ...interface{}) {
    s := fmt.Sprint(v...)
    std.Output(2, s)
    panic(s)
}

</code></pre>
<h2>18.时间</h2>
<h3>1) time.LoadLocation从文件读取</h3>
<p>这是我个人最喜欢的Go陷阱之一。要在时区之间进行转换，您首先需要加载位置信息。事实证明time.LoadLocation每次被调用时都会读取一个文件。格式化大型CSV报告的每一行时，最好的做法不是：</p>
<pre><code>package main

import (
    "testing"
    "time"
)

func BenchmarkLocation(b *testing.B) {
    for n := 0; n &lt; b.N; n++ {
        loc, _ := time.LoadLocation("Asia/Kolkata")
        time.Now().In(loc)
    }
}

func BenchmarkLocation2(b *testing.B) {
    loc, _ := time.LoadLocation("Asia/Kolkata")
    for n := 0; n &lt; b.N; n++ {
        time.Now().In(loc)
    }
}
</code></pre>
<pre><code>BenchmarkLocation-8 16810 76179 ns/op 58192 B/op 14 allocs/op
BenchmarkLocation2-8 188887110 6.97 ns/op 0 B/op 0 allocs/op
</code></pre>
<hr />
<p><a href="https://mp.weixin.qq.com/s/jUqAL7hf2GmMun64BJufEA">“Gopher部落”知识星球</a>正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，>每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需>求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：</p>
<ul>
<li>Go技术书籍的书摘和读书体会系列</li>
<li>Go与eBPF系列</li>
</ul>
<p>欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/202103/gopher-tribe-zsxq-card.png" alt="" /></p>
<p>Go技术专栏“<a href="https://www.imooc.com/read/87">改善Go语⾔编程质量的50个有效实践</a>”正在慕课网火热热销中！本专栏主要满足>广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订<br />
阅！</p>
<p><img src="https://tonybai.com/wp-content/uploads/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网热卖>中，欢迎小伙伴们订阅学习！</p>
<p><img src="https://tonybai.com/wp-content/uploads/imooc-k8s-practice-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>微信公众号：iamtonybai</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>“Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544</li>
</ul>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2021, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2021/03/29/darker-corners-of-go-part2/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Go语言的“黑暗角落”：盘点学习Go语言时遇到的那些陷阱[译]（第一部分）</title>
		<link>https://tonybai.com/2021/03/29/darker-corners-of-go-part1/</link>
		<comments>https://tonybai.com/2021/03/29/darker-corners-of-go-part1/#comments</comments>
		<pubDate>Mon, 29 Mar 2021 09:14:10 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[byte]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Const]]></category>
		<category><![CDATA[defer]]></category>
		<category><![CDATA[embed]]></category>
		<category><![CDATA[for]]></category>
		<category><![CDATA[for-range]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[gofmt]]></category>
		<category><![CDATA[goformat]]></category>
		<category><![CDATA[goimports]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[golint]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[heap]]></category>
		<category><![CDATA[IDE]]></category>
		<category><![CDATA[import]]></category>
		<category><![CDATA[init]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[iota]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[Mutex]]></category>
		<category><![CDATA[nil]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[pitfall]]></category>
		<category><![CDATA[race]]></category>
		<category><![CDATA[Reflect]]></category>
		<category><![CDATA[rune]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[Stack]]></category>
		<category><![CDATA[String]]></category>
		<category><![CDATA[strings]]></category>
		<category><![CDATA[struct]]></category>
		<category><![CDATA[sync]]></category>
		<category><![CDATA[time]]></category>
		<category><![CDATA[trap]]></category>
		<category><![CDATA[Unicode]]></category>
		<category><![CDATA[unsafe]]></category>
		<category><![CDATA[utf-8]]></category>
		<category><![CDATA[UTF8]]></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>
		<category><![CDATA[类型嵌入]]></category>
		<category><![CDATA[陷阱]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3137</guid>
		<description><![CDATA[本文翻译自Rytis Bieliunas的文章《Darker Corners of Go》。 译注：若干年前，Kyle Quest曾发过一篇名为“50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs”的文章，仿效著名的《C Traps and Pitfalls》编写了50条Go语言的陷阱与缺陷，一时在Go社区广为流传。而本文是又一篇较为系统总结Go陷阱的文章，不同于50 Shades of Go的按初中高级陷阱的分类方式，本文是按类别对Go陷阱做讲解。 0. 简介 这是什么？ 当初学习Go的时候，我只是看了一些入门书和Go语言规范。当时，我已经掌握了其他几种编程语言，然而感觉自己对Go的了解还不够，无法进行实际工作。我觉得自己对Go世界的运作方式了解地还不够深入，我可能需要趟过一些Go陷阱后才会建立起使用Go的信心。 我是对的。 虽然简单是Go语言设计哲学的核心，但当你深入使用Go时，你就会发现Go语言在用它颇具创意的方式啪啪打你的脸。 由于现在我已经用Go进行了几年的生产应用，在趟过很多“坑”之后，我想我应该将这些“遇坑与填坑”的情况整理出来献给那些Go语言的新手同学们。 我的目标是在一篇文章中收集Go中各种可能会让新开发者感到惊讶的东西，也许会对Go中比较特别的功能有所启发。我希望这能为读者节省大量的Google搜索和调试时间，并可能避免一些昂贵的错误。 我认为这篇文章对于那些至少已经知道Go语法的人来说是最有用的。如果你是一个中级或有经验的程序员，已经懂得其他编程语言，并希望学习Go，那就最好不过了。 如果你发现错误或者我没有包含你最喜欢的Go surprise，请告诉我：rytbiel@gmail.com。 非常感谢Vytautas Shaltenis的帮助，让这篇文章变得更好。 1. 代码格式化(Code formatting) 1) gofmt 在Go中，gofmt工具将许多预定好的代码格式“强加”于你的代码。gofmt对源文件进行机械性的更改，例如对包导入声明进行排序和对代码应用缩进等。这是自从切片面包诞生以来最好的事情，因为它可以节省开发人员大量无关紧要的争论所消耗的工作量。例如，它使用制表符来缩进，使用空格来对齐– 对代码风格的争论到此为止。 您可以完全不使用gofmt工具，但如果使用它，你却无法将对其所实施的代码格式化样式进行配置。该工具完全没有提供任何代码格式化选项，这才是重点。提供一种“足够好”的统一代码格式样式，它可能是没人喜欢的样式，但是Go开发人员认为统一胜于完美。 共享样式和自动代码格式化的好处包括： 无需花费任何时间在代码审查上来解决格式问题。 它可以使您免于与一起工作的同事争论大括号到底放在哪里，缩进使用制表符还是空格。你所有的激情和精力都可以得到更有效的利用。 代码更易于编写：像代码格式这样的次要工作已经有工具帮你完成。 代码更容易阅读：您无需从心理上解析你不熟悉的别人的代码格式。 大多数流行的IDE都具有Go插件，这些插件会在保存源文件时自动运行gofmt。 诸如goformat之类的第三方工具允许你在Go中使用自定义代码样式格式。但你真的希望那样做么？ 2) 长代码行 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/darker-corners-of-go-1.png" alt="" /></p>
<p>本文翻译自Rytis Bieliunas的文章<a href="https://rytisbiel.com/2021/03/06/darker-corners-of-go/">《Darker Corners of Go》</a>。</p>
<blockquote>
<p>译注：若干年前，Kyle Quest曾发过一篇名为<a href="http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/">“50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs”</a>的文章，仿效著名的<a href="https://www.cs.tufts.edu/comp/40/docs/CTrapsAndPitfalls.pdf">《C Traps and Pitfalls》</a>编写了50条Go语言的陷阱与缺陷，一时在Go社区广为流传。而本文是又一篇较为系统总结Go陷阱的文章，不同于50 Shades of Go的按初中高级陷阱的分类方式，本文是按类别对Go陷阱做讲解。</p>
</blockquote>
<h2>0. 简介</h2>
<p><strong>这是什么</strong>？</p>
<p>当初学习Go的时候，我只是看了一些入门书和<a href="https://tip.golang.org/ref/spec">Go语言规范</a>。当时，我已经掌握了其他几种编程语言，然而感觉自己对Go的了解还不够，无法进行实际工作。我觉得自己对Go世界的运作方式了解地还不够深入，我可能需要趟过一些Go陷阱后才会建立起使用Go的信心。</p>
<p><strong>我是对的</strong>。</p>
<p>虽然<a href="https://www.imooc.com/read/87/article/2321">简单是Go语言设计哲学的核心</a>，但当你深入使用Go时，你就会发现Go语言在用它颇具创意的方式啪啪打你的脸。</p>
<p>由于现在我已经用Go进行了几年的生产应用，在趟过很多“坑”之后，我想我应该将这些“遇坑与填坑”的情况整理出来献给那些Go语言的新手同学们。</p>
<p>我的目标是在一篇文章中收集Go中各种可能会让新开发者感到惊讶的东西，也许会对Go中比较特别的功能有所启发。我希望这能为读者节省大量的Google搜索和调试时间，并可能避免一些昂贵的错误。</p>
<p>我认为这篇文章对于那些至少已经知道Go语法的人来说是最有用的。如果你是一个中级或有经验的程序员，已经懂得其他编程语言，并希望学习Go，那就最好不过了。</p>
<p>如果你发现错误或者我没有包含你最喜欢的Go surprise，请告诉我：rytbiel@gmail.com。</p>
<p>非常感谢<a href="https://rtfb.lt/">Vytautas Shaltenis</a>的帮助，让这篇文章变得更好。</p>
<h2>1. 代码格式化(Code formatting)</h2>
<h3>1) gofmt</h3>
<p>在Go中，gofmt工具将许多预定好的代码格式“强加”于你的代码。gofmt对源文件进行机械性的更改，例如对包导入声明进行排序和对代码应用缩进等。这是自从切片面包诞生以来最好的事情，因为它可以节省开发人员大量无关紧要的争论所消耗的工作量。例如，它使用制表符来缩进，使用空格来对齐– 对代码风格的争论到此为止。</p>
<p>您可以完全不使用gofmt工具，但如果使用它，你却无法将对其所实施的代码格式化样式进行配置。该工具完全没有提供任何代码格式化选项，这才是重点。提供一种“足够好”的统一代码格式样式，它可能是没人喜欢的样式，但是Go开发人员认为<strong>统一胜于完美</strong>。</p>
<p>共享样式和自动代码格式化的好处包括：</p>
<ul>
<li>无需花费任何时间在代码审查上来解决格式问题。</li>
<li>它可以使您免于与一起工作的同事争论大括号到底放在哪里，缩进使用制表符还是空格。你所有的激情和精力都可以得到更有效的利用。</li>
<li>代码更易于编写：像代码格式这样的次要工作已经有工具帮你完成。</li>
<li>代码更容易阅读：您无需从心理上解析你不熟悉的别人的代码格式。</li>
</ul>
<p>大多数流行的IDE都具有Go插件，这些插件会在保存源文件时自动运行gofmt。</p>
<p>诸如goformat之类的第三方工具允许你在Go中使用自定义代码样式格式。但你真的希望那样做么？</p>
<h3>2) 长代码行</h3>
<p>Gofmt不会尝试为您分解很长的代码。有诸如golines之类的第三方工具可以做到这一点。</p>
<h3>3) 大括号</h3>
<p>在Go中，必须在行的末尾放置大括号。有趣的是，这不是gofmt强制执行的，而是Go词法分析器实现方式的副作用。有或没有gofmt，都不能将大括号放在新行上。</p>
<pre><code>package main

// missing function body
func main()
// syntax error: unexpected semicolon or newline before {
{
}

// all good!
func main() {
}
</code></pre>
<h3>4) 多行声明中的逗号</h3>
<p>在初始化切片、数组、map或结构体时，Go要求在换行符前加逗号。在多种语言中都允许使用尾部逗号，并且在某些样式指南中鼓励使用逗号。在Go中，它们是强制性的。这样在重新排列行或添加新行时就无需修改不相关的行。这也意味着更少的代码审核差异噪声。</p>
<pre><code>// all of these are OK
a := []int{1, 2}

b := []int{1, 2,}

c := []int{
    1,
    2}

d := []int{
    1,
    2,
}

// syntax error without trailing comma
e := []int{
    1,
    // syntax error: unexpected newline, expecting comma or }
    2
}
</code></pre>
<p>结构体也使用相同规则：</p>
<pre><code>type s struct {
    One int
    Two int
}

f := s{
    One: 1,
    // syntax error: unexpected newline, expecting comma or }
    Two: 2
}
</code></pre>
<h2>2. 包导入(Import)</h2>
<h3>1) 未使用的导入包</h3>
<p>未使用导入包的Go程序无法编译。这是该语言的故意设定，因为导入包会降低编译器的速度。在大型程序中，未使用的导入包可能会对编译时间产生重大影响。</p>
<p>为了使编译器在开发过程中感到happy^_^，您可以通过以下方式引用该软件包：</p>
<pre><code>package main

import (
    "fmt"
    "math"
)

// Reference unused package
var _ = math.Round 

func main() {
    fmt.Println("Hello")
}

</code></pre>
<h3>2) goimports</h3>
<p>更好的解决方案是使用goimports工具。goimports会为您删除未引用的导入包。更好的是，它尝试自动查找并添加缺失的包导入。</p>
<pre><code>package main

import "math" // imported and not used: "math"

func main() {
    fmt.Println("Hello") // undefined: fmt
}
</code></pre>
<p>运行goimports之后：</p>
<pre><code>./goimports main.go
</code></pre>
<pre><code>package main

import "fmt"

func main() {
    fmt.Println("Hello")
}
</code></pre>
<p>大多数流行的IDE的Go插件在保存源文件时会自动运行goimports。</p>
<h3>3) 下划线导入</h3>
<p>以下划线方式导入包仅是出于对其副作用的依赖。这意味着它将创建程序包级变量并运行包的<a href="https://medium.com/golangspec/init-functions-in-go-eac191b3860a">init函数</a>：</p>
<pre><code>package package1

func package1Function() int {
    fmt.Println("Package 1 side-effect")
    return 1
}

var globalVariable = package1Function()

func init() {
    fmt.Println("Package 1 init side effect")
}
</code></pre>
<p>导入package1：</p>
<pre><code>package package2

import _ package1

</code></pre>
<p>这将打印消息并初始化globalVariable：</p>
<pre><code>Package 1 side-effect
Package 1 init side effect
</code></pre>
<p>多次导入一个包（例如，在主程序包以及在其主要引用的程序包中）只运行一次该包的init函数。</p>
<p>下划线导入在Go运行时库中有使用。例如，导入net/http/pprof调用其init函数，该函数公开HTTP端点，这些端点可以提供有关应用程序的调试信息：</p>
<pre><code>import _ "net/http/pprof"

</code></pre>
<h3>4) 点导入</h3>
<p>点导入允许在不使用限定符的情况下访问导入包中的标识符：</p>
<pre><code>package main

import (
    "fmt"
    . "math"
)

func main() {
    fmt.Println(Sin(3)) // references math.Sin
}
</code></pre>
<p>是否应从Go语言中完全删除点导入一直存在公开辩论。Go团队不建议在测试包以外的任何地方使用它们：</p>
<blockquote>
<p>因为它使得程序可读性大大下降，我们很难知道一个Quux之类的名称是当前程序包中还是导入程序包中的顶层标识符 &#8211; https://golang.org/doc/faq</p>
</blockquote>
<p>另外，如果您使用go-lint工具，那么在测试文件之外使用点导入时，它会显示警告，并且您无法轻易将其关闭。</p>
<p>Go团队建议在测试中使用点可以避免包的循环依赖：</p>
<pre><code>// foo_test package tests for foo package
package foo_test

import (
    "bar/testutil" // also imports "foo"
    . "foo"
)
</code></pre>
<p>该测试文件不能成为foo包的一部分，因为它引用了bar/testutil，而bar/testutil又引用了foo并导致了循环依赖。</p>
<p>在这种情况下，首先要考虑的是，是否有一种更好的方法来构建可避免循环依赖的软件包。将bar/testutil使用的内容从foo移动到foo和bar/testutil都可以导入的第三个包可能更好，这样就可以将测试以正常方式写在foo包中。</p>
<p>如果重构没有意义，并且使用点导入将测试移至单独的程序包，则foo_test程序包至少可以假装为foo程序包的一部分。注意，它无法访问foo包的未导出类型和函数。</p>
<p>可以说，在域特定语言编程中，点导入是一个很好的用例。例如，Goa框架将其用于配置。如果没有点导入，它看起来不会很好：</p>
<pre><code>package design

import . "goa.design/goa/v3/dsl"

// API describes the global properties of the API server.
var _ = API("calc", func() {
    Title("Calculator Service")
    Description("HTTP service for adding numbers, a goa teaser")
    Server("calc", func() {
        Host("localhost", func() { URI("http://localhost:8088") })
    })
})
</code></pre>
<h2>3. 变量</h2>
<h3>1) 未使用的变量</h3>
<p>带有未使用变量的Go程序无法编译：</p>
<blockquote>
<p>如果存在未使用的变量，则可能表示有bug[…] Go拒绝使用未使用的变量或导入来编译程序，并且不会为了短期的便利性去换取更高的构建速度和程序的清晰性。- https://golang.org/doc/faq</p>
</blockquote>
<p>该规则的例外是全局变量和函数参数：</p>
<pre><code>package main

var unusedGlobal int // this is ok

func f1(unusedArg int) { // unused function arguments are also ok
    // error: a declared but not used
    a, b := 1,2
    // b is used here, but a is only assigned to, does not count as “used”
    a = b
}
</code></pre>
<h3>2) 短变量声明</h3>
<p>声明变量的简写形式仅在函数内部起作用：</p>
<pre><code>package main

v1 := 1 // error: non-declaration statement outside function body
var v2 = 2 // this is ok

func main() {
    v3 := 3 // this is ok
    fmt.Println(v3)
}
</code></pre>
<p>设置结构体字段值时，它也不起作用：</p>
<pre><code>package main

type myStruct struct {
    Field int
}

func main() {
    var s myStruct

    // error: non-name s.Field on the left side of :=
    s.Field, newVar := 1, 2 

    var newVar int
    s.Field, newVar = 1, 2 // this is actually ok
}

</code></pre>
<h3>3) 变量遮蔽</h3>
<p>令人遗憾的是，Go中允许使用变量遮蔽。您需要经常注意这一点，因为它可能导致难以发现的问题。发生这种情况是因为，为方便起见，如果至少有一个变量是新变量，Go允许使用短变量声明形式：</p>
<pre><code>package main

import "fmt"

func main() {
    v1 := 1
    // v1 is not actually redeclared here, only gets a new value set
    v1, v2 := 2, 3
    fmt.Println(v1, v2) // prints 2, 3
}
</code></pre>
<p>但是，如果声明在另一个代码块内部，则它将声明一个新变量，从而可能导致严重的错误：</p>
<pre><code>package main

import "fmt"

func main() {
    v1 := 1
    if v1 == 1 {
        v1, v2 := 2, 3
        fmt.Println(v1, v2) // prints 2, 3
    }
    fmt.Println(v1) // prints 1 !
}
</code></pre>
<p>一个更现实的示例，假设您有一个返回错误的函数：</p>
<pre><code>package main

import (
    "errors"
    "fmt"
)

func func1() error {
   return nil
}

func errFunc1() (int, error) {
   return 1, errors.New("important error")
}

func returnsErr() error {
    err := func1()
    if err == nil {
        v1, err := errFunc1()
        if err != nil {
            fmt.Println(v1, err) // prints: 1 important error
        }
    }
    return err // this returns nil!
}

func main() {
    fmt.Println(returnsErr()) // prints nil
}

</code></pre>
<p>一种解决方案是不要在嵌套代码块内使用短变量声明：</p>
<pre><code>func returnsErr() error {
    err := func1()
    var v1 int

    if err == nil {
        v1, err = errFunc1()
        if err != nil {
            fmt.Println(v1, err) // prints: 1 important error
        }
    }

    return err // returns "important error"
}

</code></pre>
<p>或者在上述示例的情况下，更好的方法是尽早退出：</p>
<pre><code>func returnsErr() error {
    err := func1()
    if err != nil {
        return err
    }

    v1, err := errFunc1()
    if err != nil {
        fmt.Println(v1, err) // prints: 1 important error
        return err
    }

    return nil
}

</code></pre>
<p>也有可以提供帮助的工具。在go vet工具中曾有一个实验性的变量遮蔽检测，后来将其删除。在撰写本文时，这是您可以安装和运行该工具的方式：</p>
<pre><code>go get -u golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow
go vet -vettool=$(which shadow)
</code></pre>
<p>打印：</p>
<pre><code>.\main.go:20:7: declaration of "err" shadows declaration at line 17
</code></pre>
<h2>4. 运算符</h2>
<h3>1) 运算符优先级</h3>
<p>Go运算符的优先级与其他语言不同：</p>
<pre><code>Precedence   Operator
5            * / % &lt;&lt; &gt;&gt; &amp; &amp;^
4            + - | ^
3            == != &lt; &lt;= &gt; &gt;=
2            &amp;&amp;
1            ||
</code></pre>
<p>将其与基于C的语言进行比较：</p>
<pre><code>Precedence   Operator
10           *, /, %
9            +, -
8            &lt;&lt;, &gt;&gt;
7            &lt;, &lt;=, &gt;, &gt;=
6            ==, !=
5            &amp;
4            ^
3            |
2            &amp;&amp;
1            ||

</code></pre>
<p>对于相同的表达式，这可能导致不同的结果：</p>
<pre><code>In Go: 1 &lt;&lt; 1 + 1 // (1&lt;&lt;1)+1 = 3
In C: 1 &lt;&lt; 1 + 1 // 1&lt;&lt;(1+1) = 4
</code></pre>
<h3>2) 自增和自减</h3>
<p>与许多其他语言不同，Go没有前缀自增或自减运算符：</p>
<pre><code>var i int
++i // syntax error: unexpected ++, expecting }
--i // syntax error: unexpected --, expecting }
</code></pre>
<p>尽管Go确实具有这些运算符的后缀版本，但Go不允许在表达式中使用它们：</p>
<pre><code>slice := []int{1,2,3}
i := 1
slice[i++] = 0 // syntax error: unexpected ++, expecting :
</code></pre>
<h3>3) 三元运算符</h3>
<p>Go语言不支持三元运算符，像下面这样的代码：</p>
<pre><code>result := a ? b : c
</code></pre>
<p>在Go中没有，你也不要费力寻找。您必须使用if-else代替。Go语言设计人员认为此运算符经常导致难看的代码，最好不要使用它。</p>
<h3>4) 按位非</h3>
<p>在Go中，XOR运算符\^被用作一元NOT运算符，而不是像许多其他语言使用〜符号。</p>
<pre><code>In Go: ^1 // -2
In C: ~1 // -2
</code></pre>
<p>用于二元计算是，XOR运算符仍用作XOR(异或)运算符。</p>
<pre><code>3^1 // 2
</code></pre>
<h2>5.常量</h2>
<h3>1) iota</h3>
<p>iota开始在Go中进行常量编号。但它并不非期望的“从零开始”，它是当前const块中常量的索引：</p>
<pre><code>const (
    myconst = "c"
    myconst2 = "c2"
    two = iota // 2
)
</code></pre>
<p>两次使用iota不会重置编号：</p>
<pre><code>const (
    zero = iota // 0
    one // 1
    two = iota // 2
)
</code></pre>
<h2>6. 切片和数组</h2>
<h3>1) 切片和数组</h3>
<p>在Go中，切片和数组的用途相似。它们的声明方式几乎相同：</p>
<pre><code>package main

import "fmt"

func main() {
    slice := []int{1, 2, 3}
    array := [3]int{1, 2, 3}
    // let the compiler work out array length
    // this will be an equivalent of [3]int
    array2 := [...]int{1, 2, 3}
    fmt.Println(slice, array, array2)
}
</code></pre>
<pre><code>[1 2 3] [1 2 3] [1 2 3]
</code></pre>
<p>切片感觉像是在顶部具有有用功能的数组。他们在实现的内部使用指向数组的指针。但是，切片要方便得多，以至于我们很少在Go中直接使用数组。</p>
<h3>2) 数组</h3>
<p>数组是有着固定大小内存的一组同类型元素的集合。不同长度的数组被认为是不同的不兼容类型。</p>
<p>与C语言不同，创建数组时，Go会将数组元素初始化为零值，因此我们无需再显式地执行此初始化操作。另外，与C不同的是，Go数组是值类型，它不是指向内存块第一个元素的指针。如果将数组传递给函数，则将复制整个数组。您仍然可以传递指向数组的指针以使其不被复制。</p>
<p><img src="https://tonybai.com/wp-content/uploads/darker-corners-of-go-2.png" alt="" /></p>
<h3>3) 切片</h3>
<p>切片是数组段的描述符。这是一个非常有用的数据结构，但可能有点不寻常。有几种可以让你掉入坑中的场景，但如果您知道切片的内部工作原理，则可以避免这些“坑”。这是Go源代码中切片的实际定义：</p>
<pre><code>type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
</code></pre>
<p><img src="https://tonybai.com/wp-content/uploads/darker-corners-of-go-3.png" alt="" /></p>
<p>Slice本身是一个值类型，但它使用指针引用它使用的数组。与数组不同，如果将切片传递给函数，则会获得数组指针，len和cap属性的副本（上图中的第一个块），但是数组本身的数据不会被复制，切片的两个副本都指向同一数组。当您“切片”一个切片时，也会发生同样的事情。Go会创建一个新的切片，该切片仍指向相同的数组：</p>
<pre><code>package main

import "fmt"

func f1(s []int) {
    // slicing the slice creates a new slice
    // but does not copy the array data
    s = s[2:4]
    // modifying the sub-slice
    // changes the array of slice in main function as well
    for i := range s {
        s[i] += 10
    }
    fmt.Println("f1", s, len(s), cap(s))
}

func main() {
    s := []int{1, 2, 3, 4, 5}
    // passing a slice as an argument
    // makes a copy of the slice properties (pointer, len and cap)
    // but the copy shares the same array
    f1(s)
    fmt.Println("main", s, len(s), cap(s))
}

</code></pre>
<pre><code>f1 [13 14] 2 3
main [1 2 13 14 5] 5 5
</code></pre>
<p><img src="https://tonybai.com/wp-content/uploads/darker-corners-of-go-4.png" alt="" /></p>
<p>如果您不知道哪个分片，则可以假设它是一个值类型，并且感到惊讶的是f1“破坏了”main中切片中的数据。</p>
<h3>4) 获取包括其数据的切片的副本</h3>
<p>要获取切片及其数据的副本，您需要做一些工作。您可以将元素手动复制到新切片或使用复制(copy)或追加(append)：</p>
<pre><code>package main

import "fmt"

func f1(s []int) {
    s = s[2:4]
    s2 := make([]int, len(s))
    copy(s2, s)

    // or if you prefer less efficient, but more concise version:
    // s2 := append([]int{}, s[2:4]...)

    for i := range s2 {
        s2[i] += 10
    }

    fmt.Println("f1", s2, len(s2), cap(s2))
}

func main() {
    s := []int{1, 2, 3, 4, 5}
    f1(s)
    fmt.Println("main", s, len(s), cap(s))
}

</code></pre>
<pre><code>f1 [13 14] 2 3
main [1 2 3 4 5] 5 5
</code></pre>
<h3>5) 使用append扩充切片</h3>
<p>切片的所有副本都共享同一数组，直到他们不这样做。切片最有用的属性是它可以为您自动管理数组的增长。当它需要超过现有数组容量时，它会分配一个全新的数组。如果您希望切片的两个副本共享数组，那么这也可能是陷阱：</p>
<pre><code>package main

import "fmt"

func main() {
    // make a slice with length 3 and capacity 4
    s := make([]int, 3, 4)

    // initialize to 1,2,3
    s[0] = 1
    s[1] = 2
    s[2] = 3

    // capacity of the array is 4
    // adding one more number fits in the initial array
    s2 := append(s, 4)

    // modify the elements of the array
    // s and s2 still share the same array
    for i := range s2 {
        s2[i] += 10
    }

    fmt.Println(s, len(s), cap(s))    // [11 12 13] 3 4
    fmt.Println(s2, len(s2), cap(s2)) // [11 12 13 14] 4 4

    // this append grows the array past its capacity
    // new array must be allocated for s3
    s3 := append(s2, 5)

    // modify the elements of the array to see the result
    for i := range s3 {
        s3[i] += 10
    }

    fmt.Println(s, len(s), cap(s)) // still the old array [11 12 13] 3 4
    fmt.Println(s2, len(s2), cap(s2)) // the old array [11 12 13 14] 4 4

    // array was copied on last append [21 22 23 24 15] 5 8
    fmt.Println(s3, len(s3), cap(s3))
}

</code></pre>
<p><img src="https://tonybai.com/wp-content/uploads/darker-corners-of-go-5.png" alt="" /></p>
<h3>6) nil切片</h3>
<p>无需检查切片是否为nil值，也不必对其初始化。len，cap和append等功能在nil slice上同样可以正常工作：</p>
<pre><code>package main

import "fmt"

func main() {
    var s []int // nil slice
    fmt.Println(s, len(s), cap(s)) // [] 0 0
    s = append(s, 1)
    fmt.Println(s, len(s), cap(s)) // [1] 1 1
}
</code></pre>
<p>空切片(empty slice)与nil切片不是同一回事：</p>
<pre><code>package main

import "fmt"

func main() {
    var s []int // this is a nil slice
    s2 := []int{} // this is an empty slice

    // looks like the same thing here:
    fmt.Println(s, len(s), cap(s)) // [] 0 0
    fmt.Println(s2, len(s2), cap(s2)) // [] 0 0

    // but s2 is actually allocated somewhere
    fmt.Printf("%p %p", s, s2) // 0x0 0x65ca90
}

</code></pre>
<p>如果您非常在意性能和内存使用情况，那么初始化一个空切片可能不如使用nil切片理想。</p>
<h3>7) make陷阱</h3>
<p>要创建一个新的切片，可以将make与切片类型以及切片的初始长度和容量一起使用。容量参数是可选的：</p>
<pre><code>func make([]T, len, cap) []T
</code></pre>
<p>这样做太简单了：</p>
<pre><code>package main

import (
    "fmt"
)

func main() {
    s := make([]int, 3)
    s = append(s, 1)
    s = append(s, 2)
    s = append(s, 3)
    fmt.Println(s)
}

</code></pre>
<pre><code>[0 0 0 1 2 3]
</code></pre>
<p>不，这永远不会发生在我身上，我知道make创建切片的第二个参数是长度，而不是容量，我听到你说……</p>
<h3> <img src='https://tonybai.com/wp-includes/images/smilies/icon_cool.gif' alt='8)' class='wp-smiley' /> 未使用的切片的数组数据</h3>
<p>由于对数组进行切片会创建一个新的切片，但会共享底层数组，因此有可能在内存中保留比你预期更多的数据。这是一个愚蠢的例子：</p>
<pre><code>package main

import (
    "bytes"
    "fmt"
    "io/ioutil"
    "os"
)

func getExecutableFormat() []byte {
    // read our own executable file into memory
    bytes, err := ioutil.ReadFile(os.Args[0])
    if err != nil {
        panic(err)
    }
    return bytes[:4]
}

func main() {
    format := getExecutableFormat()
    if bytes.HasPrefix(format, []byte("ELF")) {
        fmt.Println("linux executable")
    } else if bytes.HasPrefix(format, []byte("MZ")) {
        fmt.Println("windows executable")
    }
}
</code></pre>
<p>在上面的代码中，只要该format变量在范围内并且不能被垃圾回收，则整个可执行文件（可能几兆字节的数据）将必须保留在内存中。要修复它，请复制实际需要的字节。</p>
<h3>9) 多维切片</h3>
<p>目前，Go中没有这样的东西。可能某天会有，但是此时此刻您需要自己计算元素索引来手动将一维切片用作多维切片，或者使用“锯齿状”切片（锯齿状切片是切片的切片）：</p>
<pre><code>package main

import "fmt"

func main() {
    x := 2
    y := 3
    s := make([][]int, y)
    for i := range s {
        s[i] = make([]int, x)
    }
    fmt.Println(s)
}

</code></pre>
<pre><code>[[0 0] [0 0] [0 0]]
</code></pre>
<p>第二部分见下面链接：</p>
<ul>
<li><a href="https://tonybai.com/2021/03/29/darker-corners-of-go-part1/">Go语言的“黑暗角落”：盘点学习Go语言时遇到的那些陷阱[译]（第二部分）</a></li>
</ul>
<hr />
<p><a href="https://mp.weixin.qq.com/s/jUqAL7hf2GmMun64BJufEA">“Gopher部落”知识星球</a>正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，>每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需>求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：</p>
<ul>
<li>Go技术书籍的书摘和读书体会系列</li>
<li>Go与eBPF系列</li>
</ul>
<p>欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/202103/gopher-tribe-zsxq-card.png" alt="" /></p>
<p>Go技术专栏“<a href="https://www.imooc.com/read/87">改善Go语⾔编程质量的50个有效实践</a>”正在慕课网火热热销中！本专栏主要满足>广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订<br />
阅！</p>
<p><img src="https://tonybai.com/wp-content/uploads/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网热卖>中，欢迎小伙伴们订阅学习！</p>
<p><img src="https://tonybai.com/wp-content/uploads/imooc-k8s-practice-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>微信公众号：iamtonybai</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>“Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544</li>
</ul>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2021, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2021/03/29/darker-corners-of-go-part1/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>图解中文字符编码-Go语言例解</title>
		<link>https://tonybai.com/2019/11/07/non-ascii-character-encoding-illustrated/</link>
		<comments>https://tonybai.com/2019/11/07/non-ascii-character-encoding-illustrated/#comments</comments>
		<pubDate>Thu, 07 Nov 2019 06:54:03 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[ASCII]]></category>
		<category><![CDATA[codepoint]]></category>
		<category><![CDATA[GB18030]]></category>
		<category><![CDATA[GB2312]]></category>
		<category><![CDATA[GBK]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[rune]]></category>
		<category><![CDATA[Unicode]]></category>
		<category><![CDATA[UTF-16]]></category>
		<category><![CDATA[UTF-32]]></category>
		<category><![CDATA[UTF8]]></category>
		<category><![CDATA[万国码]]></category>
		<category><![CDATA[字符集]]></category>
		<category><![CDATA[字符集编码]]></category>
		<category><![CDATA[码点]]></category>
		<category><![CDATA[统一码]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2812</guid>
		<description><![CDATA[今天几个同事在处理一个有关中文字符编码的问题，感觉他们对字符编码这件事依然理解不够透彻。这里用图文方式对中文字符编码做一个简要的解释，例子使用Go语言。 我们知道每个英文字母和数字在计算机中都会对应一个字节，或者说用一个字节来表示，这就是最初的ASCII码。但是随着计算机在全球范围内的广泛使用，非英语国家也要在计算机使用自己的字符，于是出现了字符集“百花齐放”的情况，我国在早期也颁布了自己的中文字符集标准。字符集一多，难免出现字符集编码不兼容的情况，比如：A字符集中某字符X的编码值是Y，但是在B字符集中Y这个值所表示的字符却是Z，这种不兼容的情况在一段时间内长期存在，导致因字符集导致的传输、处理、呈现、存储等问题常常发生，非常恼人。直到Unicode(万国码/统一码)在1994年发布，人类终于有了以统一人类所有字符为目的的统一字符集。Unicode的普及也是花费了不少的时间。但在2019年的今天，世界上绝大多数系统都支持了Unicode。 Unicode究竟是啥？Unicode就是一个表，如下图： 图：unicode是什么 我们看到这个表中有两列：序号和字符。其中序号就是为全世界所有国家的所有语言文字的符号做的编码，每个字符分配一个序号，序号的范围从0&#215;000000到0x10FFFF，一共110多万个字符，这个序号也被称为Unicode码点(code point)。第二列的字符就称为“Unicode字符”。注意：同样一个“中”字，在Unicode表中的”中”称为Unicode字符“中”；在GB18030码表中的“中”称为GB18030字符“中”。计算机中的字符是有字符集属性的，因此虽然字符外形相同（都是“中”），但在计算机内部的存储表示是不同的。 图：拉丁字符对应的unicode表段 试想一下如果全世界的计算机系统都将Unicode序号作为Unicode字符的编码方案进行编解码，那么字符集问题便会从地球上彻底消失。但这个“理想的情况”并未发生。原因是什么呢？原因就是如果按照”理想方案”编码，那么无论是世界上最常用的26个字母a-z还是亚马逊森林中某个尚处于原始社会形态的某个部落的一个符号都要用一个”三字节”的存储单元表示，这意味着现实世界中所有数字资料的存储空间要变为原先的三倍（注：世界上大部分资料是用英语的26个字母编写的，原先每个字母仅需一个字节存储）、在传输相同信息的情况下，传输压力增加为原来的三倍，这是世界所无法接受的。Unicode组织其实也没有要求大家使用这种“理想的编码方案”对Unicode字符进行编码。于是就出现了UTF-8、UTF-16等变长的Unicode字符的编码方案，专门用于在存储和传输Unicode字符时使用。其中UTF-8经过实践，已经成为如今世界的Unicode字符的编码方案事实标准。 图：凤凰网默认采用utf-8编码方案 UTF-8这种Unicode字符的编码方案有几个特点： 使用变长字节对Unicode字符进行编码。采用什么编码与Unicode字符的序号有关，序号小的使用的字节就少，序号大的使用的字节就多。使用的字节个数从 1 到 4 个不等。 兼容ASCII字符集编码。这点非常重要，这意味着采用Unicode字符集时，已有的ASCII字符存储和传输方式无需改变，依然兼容可用。 UTF-8 的编码单元为一个字节（也就是一次编解码一个字节），所以在处理UTF8字符的时候就不需要考虑这一个字节的存储是在高位还是在低位。 下面我们结合图、代码示例来更清晰地了解一下Unicode字符、UTF-8编码、GB18030编码的区别。 图: “中国人”三个字对应Unicode字符、字符对应的码点（序号）、UTF-8编码与GB18030编码 从上图中，我们看到三个Unicode字符：中、国、人对应的在Unicode表中的序号(码点）分别是：U+4E2D、U+56FD和U+4EBA。我们可以通过一段Go代码来输出Unicode字符的码点。 package main import "fmt" func main() { var s = "中国人" for _, v := range s { fmt.Printf("%s =&#62; 码点：%X\n", string(v), v) } } 运行该程序的输出结果： 中 =&#62; 码点：4E2D 国 =&#62; 码点：56FD 人 [...]]]></description>
			<content:encoded><![CDATA[<p>今天几个同事在处理一个有关中文字符编码的问题，感觉他们对<a href="https://tonybai.com/2007/11/03/also-talk-about-char-encoding/">字符编码</a>这件事依然理解不够透彻。这里用图文方式对中文字符编码做一个简要的解释，例子使用<a href="https://tonybai.com/tag/go">Go语言</a>。</p>
<p>我们知道每个英文字母和数字在计算机中都会对应一个字节，或者说用一个字节来表示，这就是最初的<a href="https://www.asciitable.com/">ASCII码</a>。但是随着计算机在全球范围内的广泛使用，非英语国家也要在计算机使用自己的字符，于是出现了字符集“百花齐放”的情况，我国在早期也颁布了自己的中文字符集标准。字符集一多，难免出现字符集编码不兼容的情况，比如：A字符集中某字符X的编码值是Y，但是在B字符集中Y这个值所表示的字符却是Z，这种不兼容的情况在一段时间内长期存在，导致因字符集导致的传输、处理、呈现、存储等问题常常发生，非常恼人。直到<a href="https://home.unicode.org/">Unicode(万国码/统一码)</a>在1994年发布，人类终于有了以统一人类所有字符为目的的统一字符集。Unicode的普及也是花费了不少的时间。但在2019年的今天，世界上绝大多数系统都支持了Unicode。</p>
<p>Unicode究竟是啥？Unicode就是一个表，如下图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/non-ascii-character-encoding-illustrated-4.png" alt="img{512x368}" /></p>
<p>图：unicode是什么</p>
<p>我们看到这个表中有两列：序号和字符。其中<strong>序号</strong>就是为全世界所有国家的所有语言文字的符号做的编码，每个字符分配一个序号，序号的范围从0&#215;000000到0x10FFFF，一共110多万个字符，这个序号也被称为Unicode码点(code point)。第二列的字符就称为“Unicode字符”。注意：同样一个“中”字，在Unicode表中的”中”称为Unicode字符“中”；在GB18030码表中的“中”称为GB18030字符“中”。计算机中的字符是有<strong>字符集属性</strong>的，因此虽然字符外形相同（都是“中”），但在计算机内部的存储表示是不同的。</p>
<p><img src="https://tonybai.com/wp-content/uploads/non-ascii-character-encoding-illustrated-2.png" alt="img{512x368}" /></p>
<p>图：拉丁字符对应的unicode表段</p>
<p>试想一下如果全世界的计算机系统都将Unicode序号作为Unicode字符的编码方案进行编解码，那么字符集问题便会从地球上彻底消失。但这个“理想的情况”并未发生。原因是什么呢？原因就是如果按照”理想方案”编码，那么无论是世界上最常用的26个字母a-z还是亚马逊森林中某个尚处于原始社会形态的某个部落的一个符号都要用一个”三字节”的存储单元表示，这意味着现实世界中所有数字资料的存储空间要变为原先的三倍（注：世界上大部分资料是用英语的26个字母编写的，原先每个字母仅需一个字节存储）、在传输相同信息的情况下，传输压力增加为原来的三倍，这是世界所无法接受的。Unicode组织其实也没有要求大家使用这种“<strong>理想的编码方案</strong>”对Unicode字符进行编码。于是就出现了UTF-8、UTF-16等变长的Unicode字符的编码方案，专门用于在<strong>存储和传输Unicode字符时使用</strong>。其中UTF-8经过实践，已经成为如今世界的Unicode字符的编码方案事实标准。</p>
<p><img src="https://tonybai.com/wp-content/uploads/non-ascii-character-encoding-illustrated-3.png" alt="img{512x368}" /></p>
<p>图：凤凰网默认采用utf-8编码方案</p>
<p>UTF-8这种Unicode字符的编码方案有几个特点：</p>
<ul>
<li>
<p>使用变长字节对Unicode字符进行编码。采用什么编码与Unicode字符的序号有关，序号小的使用的字节就少，序号大的使用的字节就多。使用的字节个数从 1 到 4 个不等。</p>
</li>
<li>
<p>兼容ASCII字符集编码。这点非常重要，这意味着采用Unicode字符集时，已有的ASCII字符存储和传输方式无需改变，依然兼容可用。</p>
</li>
<li>
<p>UTF-8 的编码单元为一个字节（也就是一次编解码一个字节），所以在处理UTF8字符的时候就不需要考虑这一个字节的存储是在高位还是在低位。</p>
</li>
</ul>
<p>下面我们结合图、代码示例来更清晰地了解一下Unicode字符、UTF-8编码、<a href="http://www.gb18030.com/">GB18030编码</a>的区别。</p>
<p><img src="https://tonybai.com/wp-content/uploads/non-ascii-character-encoding-illustrated-1.png" alt="img{512x368}" /></p>
<p>图: “中国人”三个字对应Unicode字符、字符对应的码点（序号）、UTF-8编码与GB18030编码</p>
<p>从上图中，我们看到三个Unicode字符：中、国、人对应的在Unicode表中的序号(码点）分别是：U+4E2D、U+56FD和U+4EBA。我们可以通过一段Go代码来输出Unicode字符的码点。</p>
<pre><code>package main

import "fmt"

func main() {
        var s = "中国人"
        for _, v := range s {
                fmt.Printf("%s =&gt; 码点：%X\n", string(v), v)
        }
}

</code></pre>
<p>运行该程序的输出结果：</p>
<pre><code>中 =&gt; 码点：4E2D
国 =&gt; 码点：56FD
人 =&gt; 码点：4EBA

</code></pre>
<p>我们知道在Go语言中，rune这种builtin类型被用来表示一个<strong>“Unicode字符”</strong>，因此一个rune的值就是其对应Unicode字符的序号，即码点。通过for range语句对字符串进行迭代访问是，range会依次返回Unicode字符对应的rune，即码点。这里可以看到Unicode字符“中”对应的rune（码点）为0x4E2D。</p>
<p>前面我们说过，Unicode字符在存储和传输时采用的并非“理想编码方案”，而多维UTF-8编码，也就是说在上面的例子中“中国人”这三个Unicode字符在内存中并不是以码点值存储的，而是以UTF-8编码后的值存储的。还以Unicode字符“中”为例，在上图中，我们看到其对应的UTF-8编码为0xE4B8AD这三个字节，我们用Go代码来验证一下：</p>
<pre><code>package main

import "fmt"

func main() {
        var s = "中"
        fmt.Printf("%s =&gt; UTF8编码: ", s)
        for _, v := range []byte(s) {
                fmt.Printf("%X", v)
        }
        fmt.Printf("\n")
}

</code></pre>
<p>运行该程序得到如下结果：</p>
<pre><code>中 =&gt; UTF8编码: E4B8AD

</code></pre>
<p>我们将字符串转换为对应的切片元素，然后按字节逐一输出便得到了Unicode字符“中”所对应的UTF-8编码，即存储“中”这个字符时，内存所使用的字节(三个)和对应的值。</p>
<p>“中”这个字符也存在于我们的国标GB18030编码表中，那么GB18030表中是如何对GB18030字符“中”进行编码的呢？我们来看一个全面些的例子：</p>
<pre><code>// github.com/bigwhite/experiments/non-ascii-char-encoding/demo1.go

package main

import (
        "fmt"

        utils "github.com/bigwhite/gocmpp/utils"
)

func main() {
        var stringLiteral = "中国人"
        var stringUsingRuneLiteral = "\u4E2D\u56FD\u4EBA"

        if stringLiteral != stringUsingRuneLiteral {
                fmt.Println("stringLiteral is not equal to stringUsingRuneLiteral")
                return
        }
        fmt.Println("stringLiteral is equal to stringUsingRuneLiteral")

        for i, v := range stringLiteral {
                fmt.Printf("中文字符: %s &lt;=&gt; Unicode码点(rune): %X &lt;=&gt; UTF8编码(内存值): ", string(v), v)
                s := stringLiteral[i : i+3]
                for _, v := range []byte(s) {
                        fmt.Printf("0x%X ", v)
                }

                s1, _ := utils.Utf8ToGB18030(s)
                fmt.Printf("&lt;=&gt; GB18030编码(内存值): ")
                for _, v := range []byte(s1) {
                        fmt.Printf("0x%X ", v)
                }
                fmt.Printf("\n")
        }
}

</code></pre>
<p>运行该程序，得到如下结果：</p>
<pre><code>$go run demo1.go
stringLiteral is equal to stringUsingRuneLiteral
中文字符: 中 &lt;=&gt; Unicode码点(rune): 4E2D &lt;=&gt; UTF8编码(内存值): 0xE4 0xB8 0xAD &lt;=&gt; GB18030编码(内存值): 0xD6 0xD0
中文字符: 国 &lt;=&gt; Unicode码点(rune): 56FD &lt;=&gt; UTF8编码(内存值): 0xE5 0x9B 0xBD &lt;=&gt; GB18030编码(内存值): 0xB9 0xFA
中文字符: 人 &lt;=&gt; Unicode码点(rune): 4EBA &lt;=&gt; UTF8编码(内存值): 0xE4 0xBA 0xBA &lt;=&gt; GB18030编码(内存值): 0xC8 0xCB

</code></pre>
<p>我们看到，如果使用<a href="https://www.qqxiuzi.cn/zh/hanzi-gb18030-bianma.php">GB18030编码</a>，中文字符“中”字仅需要在内存中使用两个字节0xD6和0xD0表示。</p>
<p>综上，关于中文字符编码，需记住以下要点：</p>
<ul>
<li>
<p>Unicode是目前被支持最为广泛的<strong>字符集</strong>；</p>
</li>
<li>
<p>Utf-8是目前被支持最为广泛的<strong>Unicode字符的编码方式</strong>(还有其他方式，比如UTF-16、UTF-32等)；</p>
</li>
<li>
<p>针对同一个字符，比如：“中”，如果该字符存在于两个字符集编码方案A（比如：utf8)和B(比如gb18030)中，那么我们可以通过转换，将该字符在A中的编码(如：”中”的E4B8AD)转换为在B中的编码(如“中”的D6D0)。</p>
</li>
</ul>
<h2>>本文涉及的例子源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/non-ascii-char-encoding">这里</a>下载。</h2>
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网上线了，感谢小伙伴们学习支持！</p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2019, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2019/11/07/non-ascii-character-encoding-illustrated/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Hello，Termux</title>
		<link>https://tonybai.com/2017/11/09/hello-termux/</link>
		<comments>https://tonybai.com/2017/11/09/hello-termux/#comments</comments>
		<pubDate>Thu, 09 Nov 2017 13:51:58 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AArch64]]></category>
		<category><![CDATA[addon]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[apk4fun]]></category>
		<category><![CDATA[apt]]></category>
		<category><![CDATA[ARM]]></category>
		<category><![CDATA[Bash]]></category>
		<category><![CDATA[connectbot]]></category>
		<category><![CDATA[Debian]]></category>
		<category><![CDATA[F-Droid]]></category>
		<category><![CDATA[geek]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.9.2]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[hacker's-keyboard]]></category>
		<category><![CDATA[id_rsa.pub]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[logitech]]></category>
		<category><![CDATA[Lua]]></category>
		<category><![CDATA[MIX2]]></category>
		<category><![CDATA[neocomplete]]></category>
		<category><![CDATA[oh-my-zsh]]></category>
		<category><![CDATA[openssh]]></category>
		<category><![CDATA[openssl]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[root]]></category>
		<category><![CDATA[Shell]]></category>
		<category><![CDATA[sources.list]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[sshd]]></category>
		<category><![CDATA[terminal]]></category>
		<category><![CDATA[terminal-emulator]]></category>
		<category><![CDATA[Terminal-Emulator-for-Android]]></category>
		<category><![CDATA[termux]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[ultisnips]]></category>
		<category><![CDATA[UTF8]]></category>
		<category><![CDATA[Vim]]></category>
		<category><![CDATA[vim-go]]></category>
		<category><![CDATA[vim8.0]]></category>
		<category><![CDATA[vt100]]></category>
		<category><![CDATA[zsh]]></category>
		<category><![CDATA[安卓]]></category>
		<category><![CDATA[极客]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2454</guid>
		<description><![CDATA[程序员或多或少都有一颗Geek(极客)的心^0^。- Tony Bai 折腾开始。 这一切都源于前不久将手机换成了Xiaomi的MIX2。因为青睐开放的系统（相对于水果公司系统的封闭，当然Mac笔记本除外^0^），我长期使用Android平台的手机。但之前被三星Note3手机的“大屏”搞的不是很舒服，这两年一直用5寸及以下的手机，因为单手操作体验良好。MIX2的所谓“全面屏”概念又让我回归到了大屏时代。 除了大屏，现在手机“豪华”的硬件配置也让人惊叹：高通骁龙835，8核，最高主频 2.45GHz；6GB以上的LPDDR4x的双通道大内存，怪不得微软和高通都开始合作生产基于高通ARM处理器的Win10笔记本了，这配置支撑在笔记本上办公+浏览网页绰绰有余。不过对于不怎么玩游戏的我而言，这种配置仅仅用作手机日常功能有些浪费。于是有了“mobile coding”的想法和需求，至少现在是这样想的，冲动也好，伪需求也好，先实现了再说。 一、神器Termux，不仅仅是一个terminal emulator 所谓”mobile coding”不仅仅是要通过手机ssh到服务器端进行coding，还要支持在手机上搭建一个dev环境。dev环境这个需求是以往我安装的ConnectBot等ssh client端工具所无法提供的，而其他一些terminal工具，诸如Terminal Emulator for Android仅仅提供一些shell命令的支持，适合于那些喜爱使用命令行对Android机器进行管理的”administrator”们，但对dev环境的搭建支持有限的。于是神器Termux登场了。 Termux是什么？Termux首先是一个Android terminal emulator，可以像那些terminal工具一样，提供基本的shell操作命令；除此之外更为重要的是它不仅仅是一个terminal emulator。Termux提供了一套模拟的Linux环境，你可以在无需root、无需root、无需root的情况下，像在PC linux环境下一样进行各种Linux操作，包括使用apt工具进行安装包管理、定制shell、访问网络、编写源码、编译和运行程序，甚至将手机作为反向代理、负载均衡服务器或是Web服务器，又或是做一些羞羞的hack行为等。 1、安装 Termux仅支持Android 5.0及以上版本（估计现在绝大多数android机都满足这一条件）。在国内建议使用F-Droid安装Termux（先下载安装F-Droid，再在F-Droid内部搜索Termux，然后点击安装），国内的各种安装助手很少有对这个工具的支持。或是到apk4fun下载Termux的apk包（size非常小）到手机中安装(安装时需要连接着网络)。当前Termux的最新版本为0.54。 在桌面点击安装后的Termux图标，我们就启动了一个Termux应用，见下图： 2、Termux初始环境探索 Mix2手机的Android系统使用的是Android 7.1.1版本，桌面Launcher用的是MIUI 9.1稳定版，默认的shell是bash。通过Termux，我们可以查看Android 7.1.1.使用的Linux内核版本如下： $uname -a Linux localhost 4.4.21-perf-g6a9ee37d-06186-g2b2a77b #1 SMP PREEMPT Thu Oct 26 14:55:45 CST 2017 aarch64 Android 可以看出Linux内核是4.4.21，采用的CPU arch family是ARM aarch64。 我再来看一下Termux提供的常见目录结构： Home路径： $cd ~/ $pwd [...]]]></description>
			<content:encoded><![CDATA[<p><strong><em>程序员或多或少都有一颗<a href="https://en.wikipedia.org/wiki/Geek">Geek(极客)</a>的心^0^。- Tony Bai</em></strong></p>
<p>折腾开始。</p>
<p>这一切都源于前不久将手机换成了Xiaomi的<a href="https://en.wikipedia.org/wiki/Xiaomi_Mi_MIX_2">MIX2</a>。因为青睐开放的系统（相对于水果公司系统的封闭，当然Mac笔记本除外^0^），我长期使用<a href="https://en.wikipedia.org/wiki/Android_(operating_system)">Android平台</a>的手机。但之前被三星Note3手机的“大屏”搞的不是很舒服，这两年一直用5寸及以下的手机，因为单手操作体验良好。MIX2的所谓“全面屏”概念又让我回归到了大屏时代。</p>
<p>除了大屏，现在手机“豪华”的硬件配置也让人惊叹：高通骁龙835，8核，最高主频 2.45GHz；6GB以上的LPDDR4x的双通道大内存，怪不得微软和高通都开始合作生产基于高通ARM处理器的Win10笔记本了，这配置支撑在笔记本上办公+浏览网页绰绰有余。不过对于不怎么玩游戏的我而言，这种配置仅仅用作手机日常功能有些浪费。于是有了“mobile coding”的想法和需求，至少现在是这样想的，冲动也好，伪需求也好，先实现了再说。</p>
<h2>一、神器Termux，不仅仅是一个terminal emulator</h2>
<p>所谓”mobile coding”不仅仅是要通过手机ssh到服务器端进行coding，还要支持在手机上搭建一个dev环境。dev环境这个需求是以往我安装的<a href="https://github.com/connectbot/connectbot">ConnectBot</a>等ssh client端工具所无法提供的，而其他一些terminal工具，诸如<a href="https://github.com/jackpal/Android-Terminal-Emulator">Terminal Emulator for Android</a>仅仅提供<a href="https://github.com/jackpal/Android-Terminal-Emulator/wiki/Android-Shell-Command-Reference">一些shell命令</a>的支持，适合于那些喜爱使用命令行对Android机器进行管理的”administrator”们，但对dev环境的搭建支持有限的。于是神器<a href="https://termux.com/">Termux</a>登场了。</p>
<p><a href="https://github.com/termux/termux-app">Termux</a>是什么？Termux首先是一个Android terminal emulator，可以像那些terminal工具一样，提供基本的shell操作命令；除此之外更为重要的是它不仅仅是一个terminal emulator。Termux提供了一套模拟的<a href="http://tonybai.com/tag/linux">Linux</a>环境，你可以在<strong>无需root、无需root、无需root</strong>的情况下，像在PC linux环境下一样进行各种Linux操作，包括使用<a href="https://en.wikipedia.org/wiki/APT_(Debian)">apt工具</a>进行安装包管理、定制shell、访问网络、编写源码、编译和运行程序，甚至将手机作为反向代理、负载均衡服务器或是Web服务器，又或是做一些羞羞的hack行为等。</p>
<h3>1、安装</h3>
<p>Termux仅<a href="https://github.com/termux/termux-app/issues/6">支持Android 5.0及以上版本</a>（估计现在绝大多数android机都满足这一条件）。在国内建议使用<a href="https://f-droid.org/packages/com.termux/">F-Droid</a>安装Termux（先下载安装F-Droid，再在F-Droid内部搜索Termux，然后点击安装），国内的各种安装助手很少有对这个工具的支持。或是到<a href="https://www.apk4fun.com/apk/74133/">apk4fun</a>下载Termux的apk包（size非常小）到手机中安装(安装时需要连接着网络)。当前Termux的最新版本为<a href="https://github.com/termux/termux-app/releases/tag/v0.54">0.54</a>。</p>
<p>在桌面点击安装后的Termux图标，我们就启动了一个Termux应用，见下图：</p>
<p><img src="http://tonybai.com/wp-content/uploads/termux-initial-start.jpg" alt="img{512x368}" /></p>
<h3>2、Termux初始环境探索</h3>
<p>Mix2手机的Android系统使用的是<a href="https://www.android.com/phones/">Android 7.1.1版本</a>，桌面Launcher用的是<a href="https://en.wikipedia.org/wiki/MIUI">MIUI 9.1</a>稳定版，默认的shell是<a href="http://tonybai.com/2009/02/27/make-bash-my-default-shell/">bash</a>。通过Termux，我们可以查看Android 7.1.1.使用的<a href="http://tonybai.com/2012/03/15/linux-kernel-hacking-series-kernel-config-compile-and-install/">Linux内核</a>版本如下：</p>
<pre><code>$uname -a
Linux localhost 4.4.21-perf-g6a9ee37d-06186-g2b2a77b #1 SMP PREEMPT Thu Oct 26 14:55:45 CST 2017 aarch64 Android
</code></pre>
<p>可以看出<a href="http://tonybai.com/2012/03/15/linux-kernel-hacking-series-kernel-config-compile-and-install/">Linux内核</a>是4.4.21，采用的CPU arch family是<a href="https://en.wikipedia.org/wiki/ARM_architecture">ARM</a> <a href="https://en.wikipedia.org/wiki/ARM_architecture#AArch64_features">aarch64</a>。</p>
<p>我再来看一下Termux提供的常见目录结构：</p>
<p>Home路径：</p>
<pre><code>$cd ~/
$pwd
/data/data/com.termux/files/home

//或者通过环境变量HOME获取：

$echo $HOME
/data/data/com.termux/files/home
</code></pre>
<p>长期使用Linux的朋友可能会发现，这个HOME路径好是奇怪，一般的标准<a href="https://en.wikipedia.org/wiki/Linux_distribution">Linux发行版</a>，比如<a href="http://tonybai.com/tag/ubuntu">Ubuntu</a>都是在”/home”下放置用户目录，但termux环境中HOME路径却是一个<strong>奇怪的位置</strong>。在<a href="https://wiki.termux.com/wiki/Main_Page">Termux官方Wiki</a>中，我们得到的答案是：Termux是一个prefixed system。</p>
<blockquote>
<p>这个prefix的含义我理解颇有些类似于我们在使用configure脚本时指定的&#8211;prefix参数的含义。我们在执行configure脚本时，如果不显式地给&#8211;prefix传入值，那么make install后，包将被install在<strong>标准位置</strong>；否则将被install在&#8211;prefix值所指定的位置。</p>
</blockquote>
<p>prefixed system意味着Termux中所有binaries、libraries、configs都不是放在标准的位置，比如：/usr/bin、/bin、/usr/lib、/etc等下面。Termux expose了一个特殊的环境变量:PREFIX（类似于configure &#8211;prefix参数选项)：</p>
<pre><code>$echo $PREFIX
/data/data/com.termux/files/usr

$cd $PREFIX
$ls -F
bin/  etc/  include/  lib/  libexec/  share/  tmp/  var/
</code></pre>
<p>是不是有些似曾相识？但Termux的$PREFIX路径与标准linux的根路径下的目录结构毕竟还<a href="https://wiki.termux.com/wiki/Differences_from_Linux">存在差别</a>，但有着对应关系，这种对应关系大致是：</p>
<pre><code>Termux的$PREFIX/bin  &lt;=&gt;  标准Linux环境的 /bin和/usr/bin
Termux的$PREFIX/lib  &lt;=&gt;  标准Linux环境的 /lib和/usr/lib
Termux的$PREFIX/var  &lt;=&gt;  标准Linux环境的 /var
Termux的$PREFIX/etc  &lt;=&gt;  标准Linux环境的 /etc
</code></pre>
<p>因此，基本可以认为Termux的$PREFIX/就对应于标准Linux的/路径。</p>
<h3>3、更新源和包管理</h3>
<p>Termux的牛逼之处在于它基于debian的<a href="https://en.wikipedia.org/wiki/APT_(Debian)">APT包</a>管理工具进行软件包的安装、管理和卸载，就像我们在Ubuntu下所做的那样，非常方便。</p>
<p>Termux自己<a href="http://termux.net/">维护了一个源</a>，提供各种专门为termux定制的包：</p>
<pre><code># The main termux repository:
#deb [arch=all,aarch64] http://termux.net stable main
</code></pre>
<p>同时，<a href="https://github.com/termux/termux-packages">termux-packages项目</a>为开发者和爱好者提供了构建工具和脚本，通过这些工具和脚本，我们可以将自己需要的软件包编译为可以在termux运行的版本，并补充到Termux的源之中。我大致测试了一下官方这个源还是可用的，虽然初始连接的响应很缓慢。</p>
<p>国内清华大学维护了一个<a href="https://mirror.tuna.tsinghua.edu.cn/help/termux/">Termux的镜像源</a>，你可以通过编辑 /data/data/com.termux/files/usr/etc/apt/sources.list文件或执行apt edit-sources命令编辑源(在Shell配置中添加export EDITOR=vi后，apt edit-sources才能启动编辑器进行编辑)：</p>
<pre><code># The main termux repository:
#deb [arch=all,aarch64] http://termux.net stable main
deb [arch=all,aarch64] http://mirrors.tuna.tsinghua.edu.cn/termux stable main
</code></pre>
<p>剩下的操作与Ubuntu上的一模一样，无非apt update后，利用apt install安装你想要的包。目前Termux源中都有哪些包呢？可以通过apt list命令查看：</p>
<pre><code>$apt list
Listing... Done
aapt/stable 7.1.2.33-1 aarch64
abduco/stable 0.6 aarch64
abook/stable 0.6.0pre2-1 aarch64
ack-grep/stable 2.18 all
alpine/stable 2.21 aarch64
angband/stable 4.1.0 aarch64
apache2/stable 2.4.29 aarch64
apache2-dev/stable 2.4.29 aarch64
apksigner/stable 0.4 all
apr/stable 1.6.3 aarch64
apr-dev/stable 1.6.3 aarch64
apr-util/stable 1.6.1 aarch64
apr-util-dev/stable 1.6.1 aarch64
apt/stable,now 1.2.12-3 aarch64 [installed]
apt-transport-https/stable 1.2.12-3 aarch64
... ...
zile/stable 2.4.14 aarch64
zip/stable 3.0-1 aarch64
zsh/stable,now 5.4.2-1 aarch64 [installed]
</code></pre>
<p>查看是否有需要更新的包列表：</p>
<pre><code>$apt list --upgradable
</code></pre>
<p>以安装<a href="http://tonybai.com/tag/go">golang</a>为例：</p>
<pre><code>$apt install golang
....
$go version
go version go1.9.2 android/arm64
</code></pre>
<p><img src="http://tonybai.com/wp-content/uploads/termux-apt-install-go.jpg" alt="img{512x368}" /></p>
<p>Termux源中的包似乎更新的很勤奋，<a href="http://tonybai.com/2017/07/14/some-changes-in-go-1-9/">Go 1.9.2</a>才发布没多久，这里已经是最新版本了，这点值得赞一个！</p>
<h2>二、开发环境搭建</h2>
<p>我的目标是<strong>mobile coding</strong>，需要在Termux上搭建一个dev环境，以<a href="http://tonybai.com/tag/go">Go</a>环境为例。</p>
<h3>1、sshd</h3>
<p>在搭建和配置阶段，如果直接通过Android上的软键盘操作，即便屏再大，那个体验也是较差的。我们最好通过PC连到termux上去安装和配置，这就需要我们在Termux上搭建一个<a href="https://wiki.termux.com/wiki/SSH">sshd server</a>。下面是步骤：</p>
<pre><code>$apt install openssh
$sshd
</code></pre>
<p>就这么简单，一个sshd的server就在termux的后台启动起来了。由于Termux没有root权限，无法listen数值小于1024的端口，因此termux上sshd默认的listen端口是8022。另外termux上的sshd server不支持用户名+密码的方式进行登录，只能用免密登录的方式，即将PC上的~/.ssh/id_rsa.pub写入termux上的~/.ssh/authorized_keys文件中。关于免密登录的证书生成方法和导入方式，网上资料已经汗牛充栋，这里就不赘述了。导入PC端的id_rsa.pub后，PC就可以通过下面命令登录termux了：</p>
<pre><code>$ssh 10.88.46.79  -p 8022
Welcome to Termux!

Wiki:            https://wiki.termux.com
Community forum: https://termux.com/community
IRC channel:     #termux on freenode
Gitter chat:     https://gitter.im/termux/termux
Mailing list:    termux+subscribe@groups.io

Search packages:   pkg search &lt;query&gt;
Install a package: pkg install &lt;package&gt;
Upgrade packages:  pkg upgrade
Learn more:        pkg help
</code></pre>
<p>其中10.88.46.79是手机的wlan0网卡的IP地址，可以在termux中使用ip addr命令获得:</p>
<pre><code>$ip addr show wlan0
34: wlan0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc mq state UP group default qlen 3000
    ... ...
    inet 10.88.46.79/20 brd 10.88.47.255 scope global wlan0
       valid_lft forever preferred_lft forever
    ... ...
</code></pre>
<h3>2、定制shell</h3>
<p>Termux支持多种<a href="https://wiki.termux.com/wiki/Shells">主流Shell</a>，默认的Shell是<a href="http://tonybai.com/tag/bash">Bash</a>。很多开发者喜欢<a href="https://www.zsh.org/">zsh</a> + <a href="https://github.com/robbyrussell/oh-my-zsh">oh-my-zsh</a>的组合，Termux也是支持的，安装起来也是非常简单的：</p>
<pre><code>$ apt install git
$ apt install zsh
$ git clone git://github.com/robbyrussell/oh-my-zsh.git ~/.oh-my-zsh
$ cp ~/.oh-my-zsh/templates/zshrc.zsh-template ~/.zshrc
$ chsh zsh
</code></pre>
<p>与在PC上安装和配置zsh和oh-my-zsh没什么两样，你完全可以按照你在PC上的风格定制zsh的Theme等，我用的就是默认theme，所以也无需做太多变化，顶多定制一下PROMPT(~/.oh-my-zsh/themes/robbyrussell.zsh-theme中的PROMPT变量)的格式^0^。</p>
<h3>3、安装vim-go</h3>
<p>在terminal内进行Go开发，<a href="https://github.com/fatih/vim-go">vim-go</a>是必备之神器。vim-go以及相关自动补齐、snippet插件安装在不同平台上都是大同小异的，之前写过两篇《<a href="http://tonybai.com/2014/11/07/golang-development-environment-for-vim">Golang开发环境搭建-Vim篇</a>》和《<a href="http://tonybai.com/2016/09/08/upgrade-vim-go/">vim-go更新小记</a>》，大家可以参考。</p>
<p>不过这里有一个较为关键的问题，那就是Termux官方源中的vim 8.0缺少了对python和lua的支持：</p>
<pre><code> $vim --version|grep py
+cryptv          +linebreak       -python          +viminfo
+cscope          +lispindent      -python3         +vreplace
$vim --version|grep lua
+dialog_con      -lua             +rightleft       +windows
</code></pre>
<p>而一些插件又恰需要这些内置的支持，比如<a href="https://github.com/SirVer/ultisnips/issues/707">ultisnips</a>需要vim自带py支持；<a href="https://github.com/Shougo/neocomplete.vim">neocomplete</a>又依赖vim的lua支持。这样如果你还想要补齐和snippet特性，你就需要在Termux下面自己编译Vim的源码了（configure时加上对python和lua的支持）。</p>
<h3>4、中文支持</h3>
<p>无论是PC还是Termux使用的都是UTF8的内码格式，但是在安装完vim-go后，我试着用vim编辑一些简单的源码，发现在vim中输入的中文都是乱码。这里通过一个配置解决了该问题：</p>
<pre><code>//~/.vimrc

添加一行：

set enc=utf8

</code></pre>
<p>至于其中的原理，可以参见我N年前写的《<a href="http://tonybai.com/2009/09/28/also-talk-about-vim-charset-configuration/">也谈VIM字符集编码设置</a>》一文。</p>
<h2>三、键盘适配</h2>
<p>现阶段，写代码还是需要键盘输入的（憧憬未来^0^）。</p>
<h3>1、软键盘</h3>
<p>使用原生自带的默认软键盘在terminal中用vim进行coding，那得多执着啊，尤其是在vim大量使用ESC键的情况下（我都没找到原生键盘中ESC键在哪里:(）。不过Termux倒是很具包容心，为原生软键盘提供了扩展支持：用两个上下音量键协助你输入一些原生键盘上没有或者难于输入的符号，比如（全部的模拟按键列表参见<a href="https://wiki.termux.com/wiki/Touch_Keyboard">这里</a>）：</p>
<pre><code>清理屏幕：用volume down + L 来模拟 ctrl + L
结束前台程序：用volume down + C 来模拟 ctrl + C
ESC：用volume up + E 来模拟
F1-F9: 用volume up + 1 ~ 9 来模拟

</code></pre>
<blockquote>
<p>据网友提示：volume up + Q键可以打开扩展键盘键，包括ESC、CTRL、ALT等，感谢。</p>
</blockquote>
<p>这样仅能满足临时的需要，要想更有效率的输入，我们需要<a href="https://github.com/klausw/hackerskeyboard">Hacker&#8217;s Keyboard</a>。顾名思义，Hacker&#8217;s Keyboard可以理解为专为Coding(无论出于何种目的)的人准备的。和Termux一样，你可以从<a href="https://f-droid.org/packages/org.pocketworkstation.pckeyboard/">F-droid</a>安装该工具。启动该app后，app界面上有明确的使用说明，如果依旧不明确，还可以查看这篇图文并茂的文章：《<a href="https://www.wikihow.com/Use-Hacker%27s-Keyboard">How to Use Hacker&#8217;s Keyboard</a>》。默认情况下，横屏时Hacker&#8217;s keyboard会使用”Full 5-row layout”，即全键盘，竖屏时，则是4-row layout。你可以通过“系统设置”中的“语言和输入法”配置中对其进行设置，让Hacker&#8217;s keyboard无论在横屏还是竖屏都采用全键盘（我们屏幕够大^0^）：</p>
<p><img src="http://tonybai.com/wp-content/uploads/termux-hackers-keyboard-landscape.jpg" alt="img{512x368}" /><br />
横屏</p>
<p><img src="http://tonybai.com/wp-content/uploads/termux-hackers-keyboard-portrait.jpg" alt="img{512x368}" /><br />
竖屏</p>
<p>Hacker&#8217;s Keyboard无法支持中文输入，这点是目前的缺憾，不过我个人写代码时绝少使用中文，该问题忽略不计。</p>
<h3>2、外接蓝牙键盘</h3>
<p>Hacker&#8217;s Keyboard虽然一定程度提升了Coding时的输入效率，但也仅是权宜之计，长时间大规模通过软键盘输入依旧不甚可取，外接键盘是必须的。对于手机而言，目前最好的外接连接方式就是蓝牙。蓝牙键盘市面上现在有很多种，我选择了老牌大厂<a href="https://en.wikipedia.org/wiki/Logitech">logitech</a>的<a href="https://www.logitech.com/en-us/product/multi-device-keyboard-k480">K480</a>。这款键盘缺点是便携性差点、按键有些硬，但按键大小适中；而那些超便携的蓝牙键盘普遍键帽太小，长时间Coding的体验是个问题。</p>
<p><img src="http://tonybai.com/wp-content/uploads/termux-mix2-logitech-k480.jpg" alt="img{512x368}" /></p>
<p>Termux对外接键盘的支持也是很好的，除了常规输入，通过键盘组合键Ctrl+Alt与其他字母的组合<a href="https://wiki.termux.com/wiki/Hardware_Keyboard">实现各种控制功能</a>，比如：</p>
<pre><code>ctrl + alt + c =&gt; 实现创建一个新的session；
ctrl + alt + 上箭头/下箭头 =&gt; 实现切换到上一个/下一个session的窗口；
ctrl + alt + f =&gt; 全屏
ctrl + alt +v =&gt; 粘贴
ctrl + alt + +/- =&gt; 实现窗口字体的放大/缩小

</code></pre>
<p>不过，外接键盘和Hacker&#8217;s keyboard有一个相同的问题，那就是针对Termux无法输入中文。我尝试了百度、搜狗等输入法，无论如何切换（正常在其他应用中，通过【shift + 空格】实现中英文切换）均只是输入英文。</p>
<h2>四、存储</h2>
<p>到目前为止，我们提到的路径都在termux的私有的内部存储(private internal storage)路径下，这类存储的特点是termux应用内部的、私有的，一旦termux被卸载，这些数据也将不复存在。Android下还有另外两种存储类型：shared internal storage和external storage。所谓shared internal storage是手机上所有App可以共享的存储空间，放在这个空间内的数据不会因为App被卸载掉而被删除掉；而外部存储(external storage)主要是指外部插入的SD Card的存储空间。</p>
<p>默认情况下，Termux只支持private internal storage，意味着你要做好数据备份，否则一旦误卸载termux，数据可就都丢失了;数据可以用git进行管理，并sync到云端。</p>
<p>Termux提供了一个名为<a href="https://github.com/termux/termux-packages/blob/master/packages/termux-tools/termux-setup-storage">termux-setup-storage</a>的工具，可以让你在Termux下访问和使用shared internal storage和external storage；该工具是<a href="https://github.com/termux/termux-packages/tree/master/packages/termux-tools">termux-tools</a>的一部分，你可以通过apt install termux-tools来安装这些工具。</p>
<p>执行termux-setup-storage(注意：这个命令只能在手机上执行才能弹出授权对话框，通过远程ssh登录后执行没有任何效果)时，手机会弹出一个对话框，让你确认授权：</p>
<p><img src="http://tonybai.com/wp-content/uploads/termux-setup-storage.jpg" alt="img{512x368}" /></p>
<p>一旦授权，termux-setup-storage就会在HOME目录下建立一个storage目录，该目录下的结构如下：</p>
<pre><code>➜  /data/data/com.termux/files/home $tree storage
storage
├── dcim -&gt; /storage/emulated/0/DCIM
├── downloads -&gt; /storage/emulated/0/Download
├── movies -&gt; /storage/emulated/0/Movies
├── music -&gt; /storage/emulated/0/Music
├── pictures -&gt; /storage/emulated/0/Pictures
└── shared -&gt; /storage/emulated/0

6 directories, 0 files
</code></pre>
<p>我们看到在我的termux下，termux-setup-storage在storage下建立了6个符号链接，其中shared指向shared internal storage的根目录，即/storage/emulated/0；其余几个分别指向shared下的若干功能目录，比如：相册、音乐、电影、下载等。我的手机没有插SD卡，可能也不支持（市面上大多数手机都已经不支持了），如果插了一张SD卡，那么termux-setup-storage还会在storage目录下j建立一个符号链接指向在external storage上的一个termux private folder。</p>
<p>现在你就可以把数据放在shared internal storage和external storage上了，当然你也可以在Termux下自由访问shared internal storage上的数据了。</p>
<h2>五、小结</h2>
<p>Termux还设计了支持扩展的Addon机制，支持通过各种Addon来丰富Termux功能，提升其能力，这些算是高级功能，在这篇入门文章里就先不提及了。好了，接下来我就可以开始我的mobile coding了，充分利用碎片时间。后续在使用Termux+k480的过程中如果遇到什么具体的问题，我再来做针对性的解析。</p>
<hr />
<p>微博：<a href="http://weibo.com/bigwhite20xx">@tonybai_cn</a><br />
微信公众号：iamtonybai<br />
github.com: https://github.com/bigwhite</p>
<p style='text-align:left'>&copy; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2017/11/09/hello-termux/feed/</wfw:commentRss>
		<slash:comments>19</slash:comments>
		</item>
		<item>
		<title>Golang的演化历程</title>
		<link>https://tonybai.com/2014/10/25/golang-history/</link>
		<comments>https://tonybai.com/2014/10/25/golang-history/#comments</comments>
		<pubDate>Sat, 25 Oct 2014 11:36:10 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[ANSI-C]]></category>
		<category><![CDATA[BCPL]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[Concurrency]]></category>
		<category><![CDATA[CSP]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[RobPike]]></category>
		<category><![CDATA[UTF8]]></category>
		<category><![CDATA[包]]></category>
		<category><![CDATA[并发编程]]></category>
		<category><![CDATA[标准C]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[素数筛]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1585</guid>
		<description><![CDATA[本文来自Google的Golang语言设计者之一Rob Pike大神在GopherCon2014大会上的开幕主题演讲资料&#8220;Hello, Gophers!&#8221;。Rob大神在这次分 享中用了两个生动的例子讲述了Golang的演化历程，总结了Golang到目前为止的成功因素，值得广大Golang Programmer &#38; Beginner学习和了解。这里也用了&#34;Golang的演化历程&#34;作为标题。 1、Hello Gophers! package main import &#34;fmt&#34; func main() { &#160;&#160;&#160; fmt.Printf(&#34;Hello, gophers!\n&#34;) } Rob大神的见面礼，后续会有针对这段的演化历史的陈述。 2、历史 这是一个历史性的时刻。 Golang已经获得了一定的成功，值得拥有属于自己的技术大会。 3、成功 促成这份成功的因素有许多： &#160;&#160;&#160; &#8211; 功能 &#160;&#160;&#160; &#8211; 缺少的功能 &#160;&#160;&#160; &#8211; 功能的组合 &#160;&#160;&#160; &#8211; 设计&#160;&#160;&#160; &#160;&#160;&#160; &#8211; 人 &#160;&#160;&#160; &#8211; 时间 4、案例学习：两段程序 我们来近距离回顾两段程序。 第一个是你见过的第一个Go程序，是属于你的历史时刻。 第二个是我们见过的第一个Go程序，是属于全世界所有Gophers的历史时刻。 先看第一个&#8220;hello, world&#8221; 5、hello.b main( ) { &#160;&#160;&#160; [...]]]></description>
			<content:encoded><![CDATA[<p>本文来自Google的<a href="http://golang.org">Golang</a>语言设计者之一<a href="http://en.wikipedia.org/wiki/Rob_Pike">Rob Pike</a>大神在GopherCon2014大会上的开幕主题演讲资料&ldquo;<a href="http://talks.golang.org/2014/hellogophers.slide">Hello, Gophers</a>!&rdquo;。Rob大神在这次分 享中用了两个生动的例子讲述了Golang的演化历程，总结了Golang到目前为止的成功因素，值得广大Golang Programmer &amp; Beginner学习和了解。这里也用了&quot;Golang的演化历程&quot;作为标题。</p>
<p><b>1、Hello Gophers!</b></p>
<p><font face="Courier New">package main</font></p>
<p><font face="Courier New">import &quot;fmt&quot;</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;Hello, gophers!\n&quot;)<br />
	}</font></p>
<p>Rob大神的见面礼，后续会有针对这段的演化历史的陈述。</p>
<p><b>2、历史</b></p>
<p>这是一个历史性的时刻。</p>
<p>Golang已经获得了一定的成功，值得拥有属于自己的技术大会。</p>
<p><b>3、成功</b></p>
<p>促成这份成功的因素有许多：</p>
<p>&nbsp;&nbsp;&nbsp; &#8211; 功能<br />
	&nbsp;&nbsp;&nbsp; &#8211; 缺少的功能<br />
	&nbsp;&nbsp;&nbsp; &#8211; 功能的组合<br />
	&nbsp;&nbsp;&nbsp; &#8211; 设计&nbsp;&nbsp;&nbsp;<br />
	&nbsp;&nbsp;&nbsp; &#8211; 人<br />
	&nbsp;&nbsp;&nbsp; &#8211; 时间</p>
<p><b>4、案例学习：两段程序</b></p>
<p>我们来近距离回顾两段程序。</p>
<p>第一个是你见过的第一个Go程序，是属于你的历史时刻。<br />
	第二个是我们见过的第一个Go程序，是属于全世界所有Gophers的历史时刻。</p>
<p>先看第一个&ldquo;hello, world&rdquo;</p>
<p><b>5、hello.b</b></p>
<p><font face="Courier New">main( ) {<br />
	&nbsp;&nbsp;&nbsp; extrn a, b, c;<br />
	&nbsp;&nbsp;&nbsp; putchar(a); putchar(b); putchar(c); putchar(&#39;!*n&#39;);<br />
	}<br />
	a &#39;hell&#39;;<br />
	b &#39;o, w&#39;;<br />
	c &#39;orld&#39;;</font></p>
<p>上面这段代码首先出现在1972年<a href="http://en.wikipedia.org/wiki/Brian_Kernighan">Brian W. Kernighan</a>的B语言教程中（也有另外一说是出现在那之前的BCPL语言中）。</p>
<p><b>6、hello.c</b></p>
<p><font face="Courier New">main()<br />
	{<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;hello, world&quot;);<br />
	}</font></p>
<p>上面这段代码出现在1974年Brian W. Kernighan编写的《Programming in C: A Tutorial》中。这份教程当时是作为Unix v5文档的一部分。</p>
<p><b>7、hello.c </b></p>
<p><font face="Courier New">main()<br />
	{<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;hello, world\n&quot;); //译注：与上面的hello.c相比，多了个换行符\n输出<br />
	}</font></p>
<p>这段代码首次出现在1978年Brian W. Kernighan和<a href="http://en.wikipedia.org/wiki/Dennis_Ritchie">Dennis M. Ritchie</a>合著的《<a href="http://book.douban.com/subject/4816029/">The C Programming Language</a>》一书中。</p>
<p><b>8、hello.c, 标准C草案</b></p>
<p><font face="Courier New">#include &lt;stdio.h&gt; //译注：与上面hello.c相比， 多了这个头文件包含</font></p>
<p><font face="Courier New">main()<br />
	{<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;hello, world\n&quot;);<br />
	}</font></p>
<p>这段代码出现在1988年Brian W. Kernighan和Dennis M. Ritchie合著的《<a href="http://book.douban.com/subject/1236999/">The C Programming Language</a>》第二版一书中，基于标准C草案。</p>
<p>9、hello.c，标准C89</p>
<p><font face="Courier New">#include &lt;stdio.h&gt;</font></p>
<p><font face="Courier New">main(void) //</font><font face="Courier New"><font face="Courier&lt;br /&gt;<br />
        New">译注：与上面hello.c相比，多了个void</font><br />
	{<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;hello, world\n&quot;);<br />
	}</font></p>
<p>这段代码出现在1988年Brian W. Kernighan和Dennis M. Ritchie合著的《The C Programming Language》第二版第二次修订中。</p>
<p><b>10、一两代之后&#8230;</b></p>
<p>(省略所有中间语言)</p>
<p>关于Golang的讨论开始于2007年年末。</p>
<p>第一版语言规范起草于2008年3月份。</p>
<p>用于实验和原型目的的编译器开发工作已经展开。</p>
<p>最初的编译器输出的是C代码。</p>
<p>语言规范一形成，我们就重写了编译器，输出本地代码（机器码）。</p>
<p><b>11、hello.go, 2008年6月6日</b></p>
<p><font face="Courier New">package main</font></p>
<p><font face="Courier New">func main() int {<br />
	&nbsp;&nbsp;&nbsp; print &quot;hello, world\n&quot;;<br />
	&nbsp;&nbsp;&nbsp; return 0;<br />
	}</font></p>
<p>针对首次提交代码的测试。</p>
<p>内置的print已经是当时的全部实现。main函数返回一个int类型值。<br />
	注意：print后面没有括号。</p>
<p><b>12、hello.go，2008年6月27日</b></p>
<p><font face="Courier New">package main</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; print &quot;hello, world\n&quot;;<br />
	}</font></p>
<p>当main函数返回，程序调用exit(0)。</p>
<p><b>13、hello.go，2008年8月11日</b></p>
<p><font face="Courier New">package main</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; print(&quot;hello, world\n&quot;);<br />
	}</font></p>
<p>print调用加上了括号，这时print是一个函数，不再是一个原语。</p>
<p><b>14、hello.go，2008年10月24日</b></p>
<p><font face="Courier New">package main</font></p>
<p><font face="Courier New">import &quot;fmt&quot;</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; fmt.printf(&quot;hello, world\n&quot;);<br />
	}</font></p>
<p>我们熟知并喜欢的printf来了。</p>
<p><b>15、hello.go，2009年1月15日</b></p>
<p><font face="Courier New">package main</font></p>
<p><font face="Courier New">import &quot;fmt&quot;</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;hello, world\n&quot;);<br />
	}</font></p>
<p>头母大写的函数名用作才是导出的符号。</p>
<p>16、hello.go, 2009年12约11日</p>
<p><font face="Courier New">package main</font></p>
<p><font face="Courier New">import &quot;fmt&quot;</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;hello, world\n&quot;)<br />
	}</font></p>
<p>不再需要分号。</p>
<p>这是在2009年11月10日Golang开发发布后的一次重要改变。</p>
<p>这也是当前版本的hello, world</p>
<p>我们花了些时间到达这里（32年！）</p>
<p>都是历史了！</p>
<p><b>17、不仅仅有C</b></p>
<p>我们从&quot;C&quot;开始，但Go与C相比有着巨大的不同。</p>
<p>其他一些语言影响和贯穿于Go的设计当中。</p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; C: 语句和表达式语法<br />
	&nbsp;&nbsp;&nbsp; Pascal: 声明语法<br />
	&nbsp;&nbsp;&nbsp; Modula 2, Oberon 2：包<br />
	&nbsp;&nbsp;&nbsp; CSP, Occam, Newsqueak, Limbo, Alef:&nbsp; 并发<br />
	&nbsp;&nbsp;&nbsp; BCPL: 分号规则<br />
	&nbsp;&nbsp;&nbsp; Smalltalk: 方法(method)<br />
	&nbsp;&nbsp;&nbsp; Newsqueak: &lt;-, :=<br />
	&nbsp;&nbsp;&nbsp; APL: iota</font><br />
	&nbsp;&nbsp;&nbsp;<br />
	等等。也有一些是全新发明的，例如defer、常量。</p>
<p>还有一些来自其他语言的优点和缺点：<br />
	<font face="Courier New">&nbsp;&nbsp;&nbsp; C++, C#, Java, JavaScript, LISP, Python, Scala, &#8230;</font></p>
<p><b>18、hello.go，Go 1版</b></p>
<p>将我们带到了今天。</p>
<p><font face="Courier New">package main</font></p>
<p><font face="Courier New">import &quot;fmt&quot;</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; fmt.Println(&quot;Hello, Gophers (some of whom know 中文)!&quot;)<br />
	}</font></p>
<p>我们来深入挖掘一下，把这段代码做一个拆解。</p>
<p><b>19、Hello, World的16个tokens</b></p>
<p><font face="Courier New">package<br />
	main<br />
	import<br />
	&quot;fmt&quot;<br />
	func<br />
	main<br />
	(<br />
	)<br />
	{<br />
	fmt<br />
	.<br />
	Println<br />
	(<br />
	&quot;Hello, Gophers (some of whom know 中文)!&quot;<br />
	)<br />
	}</font></p>
<p><b>20、package</b></p>
<p>早期设计讨论的主要话题：扩展性的关键</p>
<p>package是什么？来自Modula-2等语言的idea</p>
<p>为什么是package?</p>
<p>&nbsp;&nbsp;&nbsp; &#8211; 拥有编译构建所需的全部信息<br />
	&nbsp;&nbsp;&nbsp; &#8211; 没有循环依赖(import)<br />
	&nbsp;&nbsp;&nbsp; &#8211; 没有子包<br />
	&nbsp;&nbsp;&nbsp; &#8211; 包名与包路径分离<br />
	&nbsp;&nbsp;&nbsp; &#8211; 包级别可见性，而不是类型级别<br />
	&nbsp;&nbsp;&nbsp; &#8211; 在包内部，你拥有整个语言，在包外部，你只拥有包许可的东西。</p>
<p><b>21、main</b></p>
<p>一个C语言遗留风范尽显之处<br />
	最初是Main，原因记不得了。<br />
	主要的包，main函数<br />
	很特别，因为它是初始化树(initialization tree)的根(root)。</p>
<p><b>22、import</b></p>
<p>一种加载包的机制<br />
	通过编译器实现（有别于文本预处理器。译注：C语言的include是通过preprocessor实现的）<br />
	努力使其高效且线性<br />
	导入的一个包，而不是一个标识符(identifiers)集合（译注：C语言的include是将头文件里的标识符集合引入）<br />
	至于export，它曾经是一个关键字。</p>
<p><b>23、&quot;fmt&quot;</b></p>
<p>包路径(package path)只是一个字符串，并非标识符的列表。<br />
	让语言避免定义它的含义 &#8211; 适应性。(Allows the language to avoid defining what it means&mdash;adaptability)<br />
	从一开始就想要一个URL作为一个选项。（译注：类似import &quot;github.com/go/tools/xxx）<br />
	可以应付将来的发展。</p>
<p><b>24、func</b></p>
<p>一个关键字，用于引入函数(类型、变量、常量），易于编译器解析。<br />
	对于函数字面量(闭包)而言，易于解析非常重要。<br />
	顺便说一下，最初这个关键字不是func，而是function。</p>
<p>小插曲：</p>
<p>Mail thread from February 6, 2008<br />
	From: Ken Thompson <a class="moz-txt-link-rfc2396E" href="mailto:ken@google.com">&lt;ken@google.com&gt;</a>  <br />
	To: gri, r<br />
	larry and sergey came by tonight. we  talked about go for more than an hour.  they both said they liked it very much.<br />
	p.s. one of larrys comments was &quot;why isnt function spelled func?&quot;<br />
	&#8212;<br />
	From: Rob Pike <a class="moz-txt-link-rfc2396E" href="mailto:r@google.com">&lt;r@google.com&gt;</a><br />
	 To: ken, gri<br />
	fine with me. seems compatible with &#39;var&#39;.<br />
	anyway we can always say, &quot;larry said to call it &#39;func&#39;&quot;</p>
<p><b>25、main</b></p>
<p>程序执行的起点。除非它不是。（译注：main不是起点，rob大神的意思是不是指下列情形，比如go test测试包，在google app engine上的go程序不需要main）<br />
	将初始化与正常执行分离，早期计划之中的。<br />
	初始化在哪里发生的？(译注：说的是package内的func init() {..}函数吧)<br />
	回到包设计。（译注：重温golang的package设计思想）</p>
<p><b>26、()</b></p>
<p>看看，没有void<br />
	main没有返回值，由运行时来处理main的返回后的事情。<br />
	没有函数参数（命令行选项通过os包获取）<br />
	没有返回值</p>
<p>返回值以及语法</p>
<p><b>27、{</b></p>
<p>用的是大括号，而不是空格（译注：估计是与python的空格缩进对比）<br />
	同样也不是方括号。<br />
	为什么在括号后放置换行符(newline)？</p>
<p><b>28、</b><b>fmt</b></p>
<p>所有导入的标识符均限定于其导入的包。（All imported identifiers are qualified by their import.）<br />
	每个标识符要么是包或函数的本地变量，要么被类型或导入包限定。<br />
	对代码可读性的重大影响。</p>
<p>为什么是fmt，而不是format？</p>
<p><b>29、.</b></p>
<p>句号token在Go中有多少使用？（很多）<br />
	a.B的含义需要使用到类型系统<br />
	但这对于人类来说非常清晰，读起来也非常容易。<br />
	针对指针的自动转换(没有-&gt;)。</p>
<p><b>30、Println</b></p>
<p>Println，不是println，头母大写才是导出符号。<br />
	知道它是反射驱动的(reflection-driven)<br />
	可变参数函数<br />
	参数类型是(&#8230;); 2010年2月1日变成(&#8230;interface{})</p>
<p><b>31、(</b></p>
<p>传统函数语法</p>
<p><b>32、</b><b><font face="Courier New">&quot;Hello, Gophers (some of whom know 中文)!&quot;</font></b></p>
<p>UTF-8编码的源码输入。字符串字面量也自动是utf8编码格式的。</p>
<p>但什么是字符串(string)呢？</p>
<p>首批写入规范的语法规则，今天很难改变了。(blog.golang.org/strings)</p>
<p><b>33、)</b></p>
<p>没有分号<br />
	在go发布后不久我们就去除了分号<br />
	早期曾胡闹地尝试将它们（译注：指得是括号）去掉<br />
	最终接受了BCPL的方案</p>
<p><b>34、}</b></p>
<p>第一轮结束。</p>
<p>旁白：还没有讨论到的</p>
<p>&nbsp;&nbsp;&nbsp; &#8211; 类型<br />
	&nbsp;&nbsp;&nbsp; &#8211; 常量<br />
	&nbsp;&nbsp;&nbsp; &#8211; 方法<br />
	&nbsp;&nbsp;&nbsp; &#8211; interface<br />
	&nbsp;&nbsp;&nbsp; &#8211; 库<br />
	&nbsp;&nbsp;&nbsp; &#8211; 内存管理<br />
	&nbsp;&nbsp;&nbsp; &#8211; 并发（接下来将讨论）<br />
	&nbsp;&nbsp;&nbsp;<br />
	外加工具，生态系统，社区等。<br />
	语言是核心，但也只是我们故事的一部分。</p>
<p><b>35、成功</b></p>
<p>要素：<br />
	&nbsp;&nbsp;&nbsp; &#8211; 站在巨人的肩膀上(building on history)<br />
	&nbsp;&nbsp;&nbsp; &#8211; 经验之作(building on experience) 译注：最初的三个神级语言设计者<br />
	&nbsp;&nbsp;&nbsp; &#8211; 设计过程<br />
	&nbsp;&nbsp;&nbsp; &#8211; 早期idea提炼到最终的方案中<br />
	&nbsp;&nbsp;&nbsp; &#8211; 由一个小团队专门集中精力做<br />
	&nbsp;&nbsp;&nbsp;<br />
	最终：承诺<br />
	&nbsp;&nbsp;&nbsp; Go 1.0锁定了语言核心与标准库。</p>
<p><b>36、另一轮</b></p>
<p>让我们看第二个程序的类似演化过程。</p>
<p><b>37、问题：素数筛(Prime sieve)</b></p>
<p>问题来自于Communicating Sequential Processes, by C. A. R. Hoare, 1978。</p>
<p>&ldquo;问题：以升序打印所有小于10000的素数。使用一个process数组：SIEVE，其中每个process从其前驱元素输入一个素数并打印它。接下 来这个process从其前驱元素接收到一个升序数字流并将它们传给其后继元素，这个过程会剔除掉所有是最初素数整数倍的数字。</p>
<p><b>38、解决方案</b></p>
<p>在1978年的CSP论文中。（注意不是Eratosthenes筛）</p>
<p>这个优美的方案是由David Gries贡献出来的。</p>
<p><b>39、CSP</b></p>
<p>在Hoare的CSP论文中：</p>
<p><font face="Courier New">[SIEVE(i:1..100)::<br />
	&nbsp;&nbsp;&nbsp; p,mp:integer;<br />
	&nbsp;&nbsp;&nbsp; SIEVE(i - 1)?p;<br />
	&nbsp;&nbsp;&nbsp; print!p;<br />
	&nbsp;&nbsp;&nbsp; mp := p; comment mp is a multiple of p;<br />
	*[m:integer; SIEVE(i - 1)?m &rarr;<br />
	&nbsp;&nbsp;&nbsp; *[m > mp &rarr; mp := mp + p];<br />
	&nbsp;&nbsp;&nbsp; [m = mp &rarr; skip<br />
	&nbsp;&nbsp;&nbsp; ||m &lt; mp &rarr; SIEVE(i + 1)!m<br />
	]&nbsp;&nbsp; ]<br />
	||SIEVE(0)::print!2; n:integer; n := 3;<br />
	&nbsp;&nbsp;&nbsp; *[n < 10000 &rarr; SIEVE(1)!n; n := n + 2]<br />
	||SIEVE(101)::*[n:integer;SIEVE(100)?n &rarr; print!n]<br />
	||print::*[(i:0..101) n:integer; SIEVE(i)?n &rarr; ...]<br />
	] </font></p>
<p>没有channel。能处理的素数的个数是在程序中指定的。</p>
<p><b>40、Newsqueak</b></p>
<p>circa 1988。</p>
<p>Rob Pike语言设计，Tom Cargill和Doug McIlroy实现。</p>
<p>使用了channels，这样个数是可编程的。(channel这个idea从何而来？）</p>
<p><font face="Courier New">counter:=prog(end: int, c: chan of int)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; i:int;<br />
	&nbsp;&nbsp;&nbsp; for(i=2; i&lt;end; i++)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c&lt;-=i;<br />
	};</font></p>
<p><font face="Courier New">filter:=prog(prime: int, listen: chan of int, send: chan of int)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; i:int;<br />
	&nbsp;&nbsp;&nbsp; for(;;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if((i=&lt;-listen)%prime)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; send&lt;-=i;<br />
	};</font></p>
<p><font face="Courier New">sieve:=prog(c: chan of int)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; for(;;){<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; prime:=&lt;-c;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print(prime, &quot; &quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; newc:=mk(chan of int);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; begin filter(prime, c, newc);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c=newc;<br />
	&nbsp;&nbsp;&nbsp; }<br />
	};</font></p>
<p><font face="Courier New">count:=mk(chan of int);</font></p>
<p><font face="Courier New">begin counter(10000, count);<br />
	begin sieve(count);<br />
	&quot;&quot;;</font></p>
<p><b>41、sieve.go，2008年3月5日</b></p>
<p>使用go规范编写的第一个版本，可能是第二个由go编写的重要程序。</p>
<p>&gt;用于发送；&lt;用于接收。Channel是指针。Main是头字母大写的。</p>
<p><font face="Courier New">package Main</font></p>
<p><font face="Courier New">// Send the sequence 2, 3, 4, &#8230; to channel &#39;ch&#39;.<br />
	func Generate(ch *chan&gt; int) {<br />
	&nbsp;&nbsp;&nbsp; for i := 2; ; i++ {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &gt;ch = i;&nbsp;&nbsp;&nbsp; // Send &#39;i&#39; to channel &#39;ch&#39;.<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">// Copy the values from channel &#39;in&#39; to channel &#39;out&#39;,<br />
	// removing those divisible by &#39;prime&#39;.<br />
	func Filter(in *chan&lt; int, out *chan&gt; int, prime int) {<br />
	&nbsp;&nbsp;&nbsp; for ; ; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; i := &lt;in;&nbsp;&nbsp;&nbsp; // Receive value of new variable &#39;i&#39; from &#39;in&#39;.<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if i % prime != 0 {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &gt;out = i;&nbsp;&nbsp;&nbsp; // Send &#39;i&#39; to channel &#39;out&#39;.<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">// The prime sieve: Daisy-chain Filter processes together.<br />
	func Sieve() {<br />
	&nbsp;&nbsp;&nbsp; ch := new(chan int);&nbsp; // Create a new channel.<br />
	&nbsp;&nbsp;&nbsp; go Generate(ch);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // Start Generate() as a subprocess.<br />
	&nbsp;&nbsp;&nbsp; for ; ; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; prime := &lt;ch;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;%d\n&quot;, prime);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ch1 := new(chan int);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; go Filter(ch, ch1, prime);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ch = ch1;<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">func Main() {<br />
	&nbsp;&nbsp;&nbsp; Sieve();<br />
	}</font></p>
<p><b>42. sieve.go，2008年7月22日</b></p>
<p><font face="Courier New">-&lt;</font>用于发送；<font face="Courier New">-&lt;</font>用于接收。Channel仍然是指针。但现在main不是大写字母开头的了。</p>
<p><font face="Courier New">package main</font></p>
<p><font face="Courier New">// Send the sequence 2, 3, 4, &#8230; to channel &#39;ch&#39;.<br />
	func Generate(ch *chan-&lt; int) {<br />
	&nbsp;&nbsp;&nbsp; for i := 2; ; i++ {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ch -&lt; i&nbsp;&nbsp;&nbsp; // Send &#39;i&#39; to channel &#39;ch&#39;.<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">// Copy the values from channel &#39;in&#39; to channel &#39;out&#39;,<br />
	// removing those divisible by &#39;prime&#39;.<br />
	func Filter(in *chan&lt;- int, out *chan-&lt; int, prime int) {<br />
	&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; i := &lt;-in;&nbsp;&nbsp;&nbsp; // Receive value of new variable &#39;i&#39; from &#39;in&#39;.<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if i % prime != 0 {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; out -&lt; i&nbsp;&nbsp;&nbsp; // Send &#39;i&#39; to channel &#39;out&#39;.<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">// The prime sieve: Daisy-chain Filter processes together.<br />
	func Sieve() {<br />
	&nbsp;&nbsp;&nbsp; ch := new(chan int);&nbsp; // Create a new channel.<br />
	&nbsp;&nbsp;&nbsp; go Generate(ch);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // Start Generate() as a subprocess.<br />
	&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; prime := &lt;-ch;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;%d\n&quot;,&nbsp;&nbsp;&nbsp; prime);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ch1 := new(chan int);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; go Filter(ch, ch1, prime);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ch = ch1<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; Sieve()<br />
	}</font></p>
<p><b>43、sieve.go，2008年9月17日</b></p>
<p>通信操作符现在是<font face="Courier New">&lt;-</font>。channel仍然是指针。</p>
<p><font face="Courier New">package main</font></p>
<p><font face="Courier New">// Send the sequence 2, 3, 4, &#8230; to channel &#39;ch&#39;.<br />
	func Generate(ch *chan &lt;- int) {<br />
	&nbsp;&nbsp;&nbsp; for i := 2; ; i++ {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ch &lt;- i&nbsp; // Send &#39;i&#39; to channel &#39;ch&#39;.<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">// Copy the values from channel &#39;in&#39; to channel &#39;out&#39;,<br />
	// removing those divisible by &#39;prime&#39;.<br />
	func Filter(in *chan &lt;- int, out *&lt;-chan int, prime int) {<br />
	&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; i := &lt;-in;&nbsp; // Receive value of new variable &#39;i&#39; from &#39;in&#39;.<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if i % prime != 0 {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; out &lt;- i&nbsp; // Send &#39;i&#39; to channel &#39;out&#39;.<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">// The prime sieve: Daisy-chain Filter processes together.<br />
	func Sieve() {<br />
	&nbsp;&nbsp;&nbsp; ch := new(chan int);&nbsp; // Create a new channel.<br />
	&nbsp;&nbsp;&nbsp; go Generate(ch);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // Start Generate() as a subprocess.<br />
	&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; prime := &lt;-ch;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print(prime, &quot;\n&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ch1 := new(chan int);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; go Filter(ch, ch1, prime);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ch = ch1<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; Sieve()<br />
	}</font></p>
<p><b><font face="Courier New">44、sieve.go，2009年1月6日</font></b></p>
<p>引入了make内置操作符。没有指针。编码错误！（有个*被留下了，错误的参数类型）</p>
<p><font face="Courier New">package main</font></p>
<p><font face="Courier New">// Send the sequence 2, 3, 4, &#8230; to channel &#39;ch&#39;.<br />
	func Generate(ch chan &lt;- int) {<br />
	&nbsp;&nbsp;&nbsp; for i := 2; ; i++ {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ch &lt;- i&nbsp; // Send &#39;i&#39; to channel &#39;ch&#39;.<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">// Copy the values from channel &#39;in&#39; to channel &#39;out&#39;,<br />
	// removing those divisible by &#39;prime&#39;.<br />
	func Filter(in chan &lt;- int, out *&lt;-chan int, prime int) {<br />
	&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; i := &lt;-in;&nbsp; // Receive value of new variable &#39;i&#39; from &#39;in&#39;.<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if i % prime != 0 {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; out &lt;- i&nbsp; // Send &#39;i&#39; to channel &#39;out&#39;.<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">// The prime sieve: Daisy-chain Filter processes together.<br />
	func Sieve() {<br />
	&nbsp;&nbsp;&nbsp; ch := make(chan int);&nbsp; // Create a new channel.<br />
	&nbsp;&nbsp;&nbsp; go Generate(ch);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // Start Generate() as a subprocess.<br />
	&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; prime := &lt;-ch;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print(prime, &quot;\n&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ch1 := make(chan int);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; go Filter(ch, ch1, prime);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ch = ch1<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; Sieve()<br />
	}</font></p>
<p><b>45、sieve.go，2009年9月25日</b></p>
<p>第一个正确的现代版本。同样，大写头母不见了，使用了fmt。</p>
<p><font face="Courier New">package main</font></p>
<p><font face="Courier New">import &quot;fmt&quot;</font></p>
<p><font face="Courier New">// Send the sequence 2, 3, 4, &#8230; to channel &#39;ch&#39;.<br />
	func generate(ch chan&lt;- int) {<br />
	&nbsp;&nbsp;&nbsp; for i := 2; ; i++ {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ch &lt;- i;&nbsp;&nbsp;&nbsp; // Send &#39;i&#39; to channel &#39;ch&#39;.<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">// Copy the values from channel &#39;in&#39; to channel &#39;out&#39;,<br />
	// removing those divisible by &#39;prime&#39;.<br />
	func filter(src &lt;-chan int, dst chan&lt;- int, prime int) {<br />
	&nbsp;&nbsp;&nbsp; for i := range src {&nbsp;&nbsp;&nbsp; // Loop over values received from &#39;src&#39;.<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if i%prime != 0 {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; dst &lt;- i;&nbsp;&nbsp;&nbsp; // Send &#39;i&#39; to channel &#39;dst&#39;.<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">// The prime sieve: Daisy-chain filter processes together.<br />
	func sieve() {<br />
	&nbsp;&nbsp;&nbsp; ch := make(chan int);&nbsp; // Create a new channel.<br />
	&nbsp;&nbsp;&nbsp; go generate(ch);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // Start generate() as a subprocess.<br />
	&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; prime := &lt;-ch;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Print(prime, &quot;\n&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ch1 := make(chan int);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; go filter(ch, ch1, prime);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ch = ch1;<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; sieve();<br />
	}</font></p>
<p><b>46、sieve.go，2009年12月10日</b></p>
<p>分号不见了。程序已经与现在一致了。</p>
<p><font face="Courier New">package main</font></p>
<p><font face="Courier New">import &quot;fmt&quot;</font></p>
<p><font face="Courier New">// Send the sequence 2, 3, 4, &hellip; to channel &#39;ch&#39;.<br />
	func generate(ch chan&lt;- int) {<br />
	&nbsp;&nbsp;&nbsp; for i := 2; ; i++ {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ch &lt;- i&nbsp; // Send &#39;i&#39; to channel &#39;ch&#39;.<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">// Copy the values from channel &#39;src&#39; to channel &#39;dst&#39;,<br />
	// removing those divisible by &#39;prime&#39;.<br />
	func filter(src &lt;-chan int, dst chan&lt;- int, prime int) {<br />
	&nbsp;&nbsp;&nbsp; for i := range src {&nbsp; // Loop over values received from &#39;src&#39;.<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if i%prime != 0 {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; dst &lt;- i&nbsp; // Send &#39;i&#39; to channel &#39;dst&#39;.<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">// The prime sieve: Daisy-chain filter processes together.<br />
	func sieve() {<br />
	&nbsp;&nbsp;&nbsp; ch := make(chan int)&nbsp; // Create a new channel.<br />
	&nbsp;&nbsp;&nbsp; go generate(ch)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // Start generate() as a subprocess.<br />
	&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; prime := &lt;-ch<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Print(prime, &quot;\n&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ch1 := make(chan int)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; go filter(ch, ch1, prime)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ch = ch1<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; sieve()<br />
	}</font></p>
<p>这个优美的方案来自于几十年的设计过程。</p>
<p><b>47、旁边，没有讨论到的</b></p>
<p><font face="Courier New">select</font></p>
<p>真实并发程序的核心连接器（connector)<br />
	最初起源于Dijkstra的守卫命令(guarded command)<br />
	在Hoare的CSP理论实现真正并发。<br />
	经过Newsqueak、Alef、Limbo和其他语言改良后</p>
<p>2008年3月26日出现在Go版本中。</p>
<p>简单，澄清，语法方面的考虑。</p>
<p><b>48、稳定性</b></p>
<p>Sieve程序自从2009年末就再未改变过。&#8211; 稳定！</p>
<p>开源系统并不总是兼容和稳定的。</p>
<p>但，Go是。（兼容和稳定的）</p>
<p>这是Go成功的一个重要原因。</p>
<p><b>49、</b><b>趋势</b></p>
<p>图数据展示了Go 1.0发布后Go语言的爆发。</p>
<p><b>50、成功</b></p>
<p>Go成功的元素：</p>
<p>&nbsp;&nbsp;&nbsp; 显然的：功能和工具。</p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; * 并发<br />
	&nbsp;&nbsp;&nbsp; * 垃圾回收<br />
	&nbsp;&nbsp;&nbsp; * 高效的实现<br />
	&nbsp;&nbsp;&nbsp; * 给人以动态类型体验的静态类型系统<br />
	&nbsp;&nbsp;&nbsp; * 丰富但规模有限的标准库<br />
	&nbsp;&nbsp;&nbsp; * 工具化<br />
	&nbsp;&nbsp;&nbsp; * gofmt<br />
	&nbsp;&nbsp;&nbsp; * 在大规模系统中的应用</font></p>
<p>&nbsp;&nbsp;&nbsp; 不那么显然的：过程</p>
<p>&nbsp;&nbsp;&nbsp; * 始终聚焦最初的目标<br />
	&nbsp;&nbsp;&nbsp; * 在冻结后的集中开发<br />
	&nbsp;&nbsp;&nbsp; * 小核心团队易于取得一致<br />
	&nbsp;&nbsp;&nbsp; * 社区的重要贡献<br />
	&nbsp;&nbsp;&nbsp; * 丰富的生态系统<br />
	&nbsp;&nbsp;&nbsp;<br />
	总之，开源社区共享了我们的使命，聚焦于为当今的世界设计一门语言。</p>
<p style='text-align:left'>&copy; 2014, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2014/10/25/golang-history/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
